cli-claw-kit 0.0.1

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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/config/default-groups.json +1 -0
  4. package/config/global-agents-md.template.md +37 -0
  5. package/config/mount-allowlist.json +11 -0
  6. package/container/Dockerfile +160 -0
  7. package/container/agent-runner/dist/.tsbuildinfo +1 -0
  8. package/container/agent-runner/dist/agent-definitions.js +22 -0
  9. package/container/agent-runner/dist/channel-prefixes.js +16 -0
  10. package/container/agent-runner/dist/codex-config.js +29 -0
  11. package/container/agent-runner/dist/image-detector.js +96 -0
  12. package/container/agent-runner/dist/index.js +2587 -0
  13. package/container/agent-runner/dist/mcp-tools.js +1076 -0
  14. package/container/agent-runner/dist/stream-event.types.js +5 -0
  15. package/container/agent-runner/dist/stream-processor.js +867 -0
  16. package/container/agent-runner/dist/types.js +6 -0
  17. package/container/agent-runner/dist/utils.js +115 -0
  18. package/container/agent-runner/package.json +36 -0
  19. package/container/agent-runner/prompts/security-rules.md +31 -0
  20. package/container/agent-runner/src/agent-definitions.ts +27 -0
  21. package/container/agent-runner/src/channel-prefixes.ts +16 -0
  22. package/container/agent-runner/src/codex-config.ts +40 -0
  23. package/container/agent-runner/src/image-detector.ts +116 -0
  24. package/container/agent-runner/src/index.ts +3107 -0
  25. package/container/agent-runner/src/mcp-tools.ts +1295 -0
  26. package/container/agent-runner/src/stream-event.types.ts +10 -0
  27. package/container/agent-runner/src/stream-processor.ts +932 -0
  28. package/container/agent-runner/src/types.ts +75 -0
  29. package/container/agent-runner/src/utils.ts +114 -0
  30. package/container/agent-runner/tsconfig.json +17 -0
  31. package/container/build.sh +28 -0
  32. package/container/entrypoint.sh +64 -0
  33. package/container/skills/agent-browser/SKILL.md +159 -0
  34. package/container/skills/install-skill/SKILL.md +64 -0
  35. package/container/skills/post-test-cleanup/SKILL.md +121 -0
  36. package/dist/.tsbuildinfo +1 -0
  37. package/dist/agent-output-parser.js +459 -0
  38. package/dist/app-root.js +52 -0
  39. package/dist/assistant-meta-footer.js +1 -0
  40. package/dist/auth.js +91 -0
  41. package/dist/billing.js +694 -0
  42. package/dist/channel-prefixes.js +16 -0
  43. package/dist/cli.js +86 -0
  44. package/dist/commands.js +79 -0
  45. package/dist/config.js +120 -0
  46. package/dist/container-runner.js +981 -0
  47. package/dist/daily-summary.js +210 -0
  48. package/dist/db.js +3683 -0
  49. package/dist/dingtalk.js +1347 -0
  50. package/dist/feishu-markdown-style.js +97 -0
  51. package/dist/feishu-streaming-card.js +1875 -0
  52. package/dist/feishu.js +1628 -0
  53. package/dist/file-manager.js +270 -0
  54. package/dist/group-queue.js +1070 -0
  55. package/dist/group-runtime.js +35 -0
  56. package/dist/host-workspace-cwd.js +85 -0
  57. package/dist/im-channel.js +384 -0
  58. package/dist/im-command-utils.js +142 -0
  59. package/dist/im-downloader.js +45 -0
  60. package/dist/im-manager.js +527 -0
  61. package/dist/im-utils.js +53 -0
  62. package/dist/image-detector.js +96 -0
  63. package/dist/index.js +5828 -0
  64. package/dist/logger.js +22 -0
  65. package/dist/mcp-utils.js +66 -0
  66. package/dist/message-attachments.js +69 -0
  67. package/dist/message-notifier.js +36 -0
  68. package/dist/middleware/auth.js +85 -0
  69. package/dist/mount-security.js +315 -0
  70. package/dist/permissions.js +67 -0
  71. package/dist/project-memory.js +6 -0
  72. package/dist/provider-pool.js +189 -0
  73. package/dist/qq.js +826 -0
  74. package/dist/reset-admin.js +42 -0
  75. package/dist/routes/admin.js +543 -0
  76. package/dist/routes/agent-definitions.js +241 -0
  77. package/dist/routes/agents.js +533 -0
  78. package/dist/routes/auth.js +675 -0
  79. package/dist/routes/billing.js +490 -0
  80. package/dist/routes/browse.js +210 -0
  81. package/dist/routes/bug-report.js +387 -0
  82. package/dist/routes/config.js +1868 -0
  83. package/dist/routes/files.js +671 -0
  84. package/dist/routes/groups.js +1367 -0
  85. package/dist/routes/mcp-servers.js +320 -0
  86. package/dist/routes/memory.js +523 -0
  87. package/dist/routes/monitor.js +307 -0
  88. package/dist/routes/skills.js +777 -0
  89. package/dist/routes/tasks.js +509 -0
  90. package/dist/routes/usage.js +64 -0
  91. package/dist/routes/workspace-config.js +458 -0
  92. package/dist/runtime-build.js +112 -0
  93. package/dist/runtime-command-handler.js +189 -0
  94. package/dist/runtime-command-registry.js +1 -0
  95. package/dist/runtime-config.js +1777 -0
  96. package/dist/runtime-identity.js +52 -0
  97. package/dist/schemas.js +590 -0
  98. package/dist/script-runner.js +64 -0
  99. package/dist/sdk-query.js +82 -0
  100. package/dist/skill-utils.js +145 -0
  101. package/dist/sqlite-compat.js +19 -0
  102. package/dist/stream-event.types.js +5 -0
  103. package/dist/streaming-runtime-meta.js +29 -0
  104. package/dist/task-scheduler.js +695 -0
  105. package/dist/task-utils.js +13 -0
  106. package/dist/telegram-pairing.js +59 -0
  107. package/dist/telegram.js +897 -0
  108. package/dist/terminal-manager.js +307 -0
  109. package/dist/tool-step-display.js +1 -0
  110. package/dist/types.js +1 -0
  111. package/dist/utils.js +85 -0
  112. package/dist/web-context.js +161 -0
  113. package/dist/web.js +1377 -0
  114. package/dist/wechat-crypto.js +182 -0
  115. package/dist/wechat.js +589 -0
  116. package/dist/workspace-runtime-reset.js +35 -0
  117. package/package.json +107 -0
  118. package/shared/assistant-meta-footer.ts +127 -0
  119. package/shared/channel-prefixes.ts +16 -0
  120. package/shared/dist/assistant-meta-footer.d.ts +29 -0
  121. package/shared/dist/assistant-meta-footer.js +85 -0
  122. package/shared/dist/channel-prefixes.d.ts +4 -0
  123. package/shared/dist/channel-prefixes.js +16 -0
  124. package/shared/dist/image-detector.d.ts +20 -0
  125. package/shared/dist/image-detector.js +96 -0
  126. package/shared/dist/runtime-command-registry.d.ts +38 -0
  127. package/shared/dist/runtime-command-registry.js +185 -0
  128. package/shared/dist/stream-event.d.ts +65 -0
  129. package/shared/dist/stream-event.js +8 -0
  130. package/shared/dist/tool-step-display.d.ts +4 -0
  131. package/shared/dist/tool-step-display.js +11 -0
  132. package/shared/image-detector.ts +116 -0
  133. package/shared/runtime-command-registry.ts +252 -0
  134. package/shared/stream-event.ts +67 -0
  135. package/shared/tool-step-display.ts +21 -0
  136. package/shared/tsconfig.json +24 -0
  137. package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
  138. package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
  139. package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
  140. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  141. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  142. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  143. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  144. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  145. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  146. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  147. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  148. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  149. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  150. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  151. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  152. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  153. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  154. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  155. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  156. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  157. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  158. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  159. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  160. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  161. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  162. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  163. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  164. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  165. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  166. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  167. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  168. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  169. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  170. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  171. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  172. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  173. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  174. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  175. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  176. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  177. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  178. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  179. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  180. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  181. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  182. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  183. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  184. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  185. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  186. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  187. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  188. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  189. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  190. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  191. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  192. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  193. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  194. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  195. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  196. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  197. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  198. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  199. package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
  200. package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
  201. package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
  202. package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
  203. package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
  204. package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
  205. package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
  206. package/web/dist/assets/band-CquvqAHh.js +1 -0
  207. package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
  208. package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
  209. package/web/dist/assets/channel-BOVj73LR.js +1 -0
  210. package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
  211. package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
  212. package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
  213. package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
  214. package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
  215. package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
  216. package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
  217. package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
  218. package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
  219. package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
  220. package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
  221. package/web/dist/assets/clone-BmaCesfa.js +1 -0
  222. package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
  223. package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  224. package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
  225. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  226. package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
  227. package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
  228. package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
  229. package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
  230. package/web/dist/assets/error-CGD5mp5f.js +1 -0
  231. package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
  232. package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
  233. package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
  234. package/web/dist/assets/graph-CeAEckur.js +1 -0
  235. package/web/dist/assets/index-CPnL1_qC.js +768 -0
  236. package/web/dist/assets/index-DVevCbcO.css +10 -0
  237. package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
  238. package/web/dist/assets/init-Dmth1JHB.js +1 -0
  239. package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
  240. package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
  241. package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
  242. package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
  243. package/web/dist/assets/linear-DiaJloY5.js +1 -0
  244. package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
  245. package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
  246. package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
  247. package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
  248. package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
  249. package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
  250. package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
  251. package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
  252. package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
  253. package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
  254. package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
  255. package/web/dist/assets/square-0CqMX1Q3.js +11 -0
  256. package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
  257. package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
  258. package/web/dist/assets/step-D51IIHGA.js +1 -0
  259. package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
  260. package/web/dist/assets/time-O8zIGux3.js +1 -0
  261. package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
  262. package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
  263. package/web/dist/assets/utils-KGAn0XTg.js +11 -0
  264. package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
  265. package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
  266. package/web/dist/assets/zap-_hKJYy7J.js +6 -0
  267. package/web/dist/favicon.svg +332 -0
  268. package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
  269. package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
  270. package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
  271. package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
  272. package/web/dist/fonts/DMSans-latin.woff2 +0 -0
  273. package/web/dist/icons/README.md +20 -0
  274. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  275. package/web/dist/icons/icon-128.png +0 -0
  276. package/web/dist/icons/icon-144.png +0 -0
  277. package/web/dist/icons/icon-152.png +0 -0
  278. package/web/dist/icons/icon-192.png +0 -0
  279. package/web/dist/icons/icon-192.svg +332 -0
  280. package/web/dist/icons/icon-384.png +0 -0
  281. package/web/dist/icons/icon-48.png +0 -0
  282. package/web/dist/icons/icon-512-maskable.png +0 -0
  283. package/web/dist/icons/icon-512.png +0 -0
  284. package/web/dist/icons/icon-512.svg +332 -0
  285. package/web/dist/icons/icon-72.png +0 -0
  286. package/web/dist/icons/icon-96.png +0 -0
  287. package/web/dist/icons/loading-logo.svg +332 -0
  288. package/web/dist/icons/logo-1024.png +0 -0
  289. package/web/dist/icons/logo-icon.svg +332 -0
  290. package/web/dist/icons/logo-text.svg +332 -0
  291. package/web/dist/index.html +30 -0
  292. package/web/dist/manifest.webmanifest +1 -0
  293. package/web/dist/registerSW.js +1 -0
  294. package/web/dist/sw.js +1 -0
  295. package/web/dist/workbox-08d6266a.js +1 -0
@@ -0,0 +1,981 @@
1
+ /**
2
+ * Container Runner for cli-claw
3
+ * Spawns agent execution in Docker container and handles IPC
4
+ */
5
+ import { execFile, execFileSync, spawn, } from 'child_process';
6
+ import fs from 'fs';
7
+ import { createRequire } from 'module';
8
+ import os from 'os';
9
+ import path from 'path';
10
+ import { APP_ROOT, LAUNCH_CWD, resolveAppPath } from './app-root.js';
11
+ import { CONTAINER_IMAGE, DATA_DIR, GROUPS_DIR } from './config.js';
12
+ import { logger } from './logger.js';
13
+ import { loadMountAllowlist, validateAdditionalMounts, } from './mount-security.js';
14
+ import { buildContainerEnvLines, getClaudeProviderConfig, getContainerEnvConfig, getEnabledProviders, getBalancingConfig, getSystemSettings, mergeClaudeEnvConfig, resolveProviderById, shellQuoteEnvLines, writeCredentialsFile, } from './runtime-config.js';
15
+ import { providerPool } from './provider-pool.js';
16
+ import { isApiError } from './agent-output-parser.js';
17
+ import { loadUserMcpServers } from './mcp-utils.js';
18
+ import { attachStderrHandler, attachStdoutHandler, createStderrState, createStdoutParserState, formatUserFacingRuntimeError, handleNonZeroExit, handleSuccessClose, handleTimeoutClose, writeRunLog, } from './agent-output-parser.js';
19
+ import { getRuntimeBuildLogFields } from './runtime-build.js';
20
+ /**
21
+ * Required env flags for settings.json — 每次容器/进程启动时强制写入,不可被用户覆盖。
22
+ * 合并模式:仅覆盖这些 key,保留用户自定义的其他 key。
23
+ */
24
+ const REQUIRED_SETTINGS_ENV = {
25
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '0',
26
+ CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
27
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
28
+ };
29
+ /** Read existing settings.json, deep-merge required env keys and mcpServers, write only if changed */
30
+ function ensureSettingsJson(settingsFile, mcpServers) {
31
+ let existing = {};
32
+ try {
33
+ if (fs.existsSync(settingsFile)) {
34
+ existing = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
35
+ }
36
+ }
37
+ catch {
38
+ /* ignore parse errors, overwrite */
39
+ }
40
+ const existingEnv = existing.env || {};
41
+ const mergedEnv = { ...existingEnv, ...REQUIRED_SETTINGS_ENV };
42
+ const merged = { ...existing, env: mergedEnv };
43
+ // Merge user-configured MCP servers into settings
44
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
45
+ const existingMcp = existing.mcpServers || {};
46
+ merged.mcpServers = { ...existingMcp, ...mcpServers };
47
+ }
48
+ const newContent = JSON.stringify(merged, null, 2) + '\n';
49
+ // Only write when content actually changed
50
+ try {
51
+ if (fs.existsSync(settingsFile)) {
52
+ const current = fs.readFileSync(settingsFile, 'utf8');
53
+ if (current === newContent)
54
+ return;
55
+ }
56
+ }
57
+ catch {
58
+ /* write anyway */
59
+ }
60
+ fs.writeFileSync(settingsFile, newContent, { mode: 0o644 });
61
+ }
62
+ function requireWorkspaceOwner(group, context) {
63
+ if (!group.created_by) {
64
+ throw new Error(`Workspace ${group.folder} is missing created_by; ${context} requires an owned workspace`);
65
+ }
66
+ return group.created_by;
67
+ }
68
+ /**
69
+ * Create directory with 0o777 permissions for container volume mounts.
70
+ * Fixes uid mismatch between host user and container node user (uid 1000),
71
+ * especially in rootless podman where uid remapping causes permission denied.
72
+ */
73
+ function mkdirForContainer(dirPath) {
74
+ fs.mkdirSync(dirPath, { recursive: true });
75
+ try {
76
+ fs.chmodSync(dirPath, 0o777);
77
+ }
78
+ catch {
79
+ // Ignore — may fail on read-only filesystem or special mounts
80
+ }
81
+ }
82
+ /**
83
+ * Try to select a provider from the pool. Returns profileId + resolved config,
84
+ * or null if pool mode is off (≤1 enabled) / group has provider override / selection fails.
85
+ */
86
+ function trySelectPoolProvider(groupFolder) {
87
+ const override = getContainerEnvConfig(groupFolder);
88
+ const hasOverride = !!(override.anthropicApiKey ||
89
+ override.anthropicAuthToken ||
90
+ override.anthropicBaseUrl);
91
+ if (hasOverride)
92
+ return null;
93
+ // Refresh pool state from V4 config
94
+ const enabledProviders = getEnabledProviders();
95
+ if (enabledProviders.length <= 1)
96
+ return null; // No pool needed for 0-1 providers
97
+ const balancing = getBalancingConfig();
98
+ providerPool.refreshFromConfig(enabledProviders, balancing);
99
+ try {
100
+ const profileId = providerPool.selectProvider();
101
+ const resolved = resolveProviderById(profileId);
102
+ providerPool.acquireSession(profileId);
103
+ return {
104
+ profileId,
105
+ resolved: { config: resolved.config, customEnv: resolved.customEnv },
106
+ };
107
+ }
108
+ catch (err) {
109
+ logger.warn({ err }, 'Provider pool selection failed, falling back to active profile');
110
+ return null;
111
+ }
112
+ }
113
+ function buildVolumeMounts(group, isAdminHome, mountUserSkills = true, agentId, ownerHomeFolder, taskRunId, resolvedProvider) {
114
+ const mounts = [];
115
+ const packageRoot = APP_ROOT;
116
+ const launchCwd = LAUNCH_CWD;
117
+ // Per-user global memory directory:
118
+ // Each user gets their own user-global/{userId}/ mounted as /workspace/global
119
+ const ownerId = requireWorkspaceOwner(group, 'container mounts');
120
+ const userGlobalDir = path.join(GROUPS_DIR, 'user-global', ownerId);
121
+ mkdirForContainer(userGlobalDir);
122
+ mounts.push({
123
+ hostPath: userGlobalDir,
124
+ containerPath: '/workspace/global',
125
+ readonly: !group.is_home,
126
+ });
127
+ if (isAdminHome) {
128
+ // Preserve the launch directory separately from packaged resources.
129
+ // Admin home continues to see the operator's startup cwd as /workspace/project.
130
+ mounts.push({
131
+ hostPath: launchCwd,
132
+ containerPath: '/workspace/project',
133
+ readonly: false,
134
+ });
135
+ // Admin home also gets its group folder as the working directory
136
+ mounts.push({
137
+ hostPath: path.join(GROUPS_DIR, group.folder),
138
+ containerPath: '/workspace/group',
139
+ readonly: false,
140
+ });
141
+ }
142
+ else {
143
+ // Member home and non-home groups only get their own folder
144
+ mounts.push({
145
+ hostPath: path.join(GROUPS_DIR, group.folder),
146
+ containerPath: '/workspace/group',
147
+ readonly: false,
148
+ });
149
+ }
150
+ // Memory directory: home containers write their own; non-home containers read owner's home memory
151
+ const memoryFolder = group.is_home
152
+ ? group.folder
153
+ : ownerHomeFolder || group.folder;
154
+ const memoryDir = path.join(DATA_DIR, 'memory', memoryFolder);
155
+ mkdirForContainer(memoryDir);
156
+ mounts.push({
157
+ hostPath: memoryDir,
158
+ containerPath: '/workspace/memory',
159
+ readonly: !group.is_home,
160
+ });
161
+ // Per-group Claude sessions directory (isolated from other groups)
162
+ // Sub-agents get their own session dir under agents/{agentId}/.claude/
163
+ const groupSessionsDir = agentId
164
+ ? path.join(DATA_DIR, 'sessions', group.folder, 'agents', agentId, '.claude')
165
+ : path.join(DATA_DIR, 'sessions', group.folder, '.claude');
166
+ mkdirForContainer(groupSessionsDir);
167
+ const settingsFile = path.join(groupSessionsDir, 'settings.json');
168
+ const mcpServers = ownerId ? loadUserMcpServers(ownerId) : {};
169
+ ensureSettingsJson(settingsFile, mcpServers);
170
+ mounts.push({
171
+ hostPath: groupSessionsDir,
172
+ containerPath: '/home/node/.claude',
173
+ readonly: false,
174
+ });
175
+ // Skills:以只读卷挂载宿主机目录(由 entrypoint 创建符号链接)
176
+ // 用户的所有 skills 在其所有工作区中全量生效
177
+ const projectSkillsDir = path.join(packageRoot, 'container', 'skills');
178
+ const userSkillsDir = mountUserSkills && ownerId ? path.join(DATA_DIR, 'skills', ownerId) : null;
179
+ // Ensure user skills directory exists so it can always be mounted.
180
+ // Skills may be installed after the group is created; without pre-creating,
181
+ // the existsSync check would skip mounting and the container would never see them.
182
+ if (userSkillsDir) {
183
+ fs.mkdirSync(userSkillsDir, { recursive: true });
184
+ }
185
+ // 全量挂载:用户的所有 skills 在所有工作区中生效
186
+ if (fs.existsSync(projectSkillsDir)) {
187
+ mounts.push({
188
+ hostPath: projectSkillsDir,
189
+ containerPath: '/workspace/project-skills',
190
+ readonly: true,
191
+ });
192
+ }
193
+ if (userSkillsDir) {
194
+ mounts.push({
195
+ hostPath: userSkillsDir,
196
+ containerPath: '/workspace/user-skills',
197
+ readonly: true,
198
+ });
199
+ }
200
+ // Per-group IPC namespace: each group gets its own IPC directory
201
+ // Sub-agents get their own IPC subdirectory under agents/{agentId}/
202
+ // Isolated tasks get their own IPC subdirectory under tasks-run/{taskRunId}/
203
+ // Use 0o777 so container (node/1000) and host (agent/1002) can both read/write.
204
+ const groupIpcDir = agentId
205
+ ? path.join(DATA_DIR, 'ipc', group.folder, 'agents', agentId)
206
+ : taskRunId
207
+ ? path.join(DATA_DIR, 'ipc', group.folder, 'tasks-run', taskRunId)
208
+ : path.join(DATA_DIR, 'ipc', group.folder);
209
+ mkdirForContainer(groupIpcDir);
210
+ // All agents (main + sub/conversation) get agents/ subdir for spawn/message IPC
211
+ // Use chmod 777 so both host (agent/1002) and container (node/1000) can write
212
+ for (const sub of ['messages', 'tasks', 'input', 'agents']) {
213
+ const subDir = path.join(groupIpcDir, sub);
214
+ fs.mkdirSync(subDir, { recursive: true });
215
+ try {
216
+ fs.chmodSync(subDir, 0o777);
217
+ }
218
+ catch {
219
+ /* ignore if already correct */
220
+ }
221
+ }
222
+ mounts.push({
223
+ hostPath: groupIpcDir,
224
+ containerPath: '/workspace/ipc',
225
+ readonly: false,
226
+ });
227
+ // Per-container environment file (keeps credentials out of process listings)
228
+ // Global config merged with per-container overrides.
229
+ const envDir = path.join(DATA_DIR, 'env', group.folder);
230
+ fs.mkdirSync(envDir, { recursive: true });
231
+ const globalConfig = resolvedProvider?.config ?? getClaudeProviderConfig();
232
+ const containerOverride = getContainerEnvConfig(group.folder);
233
+ const envLines = buildContainerEnvLines(globalConfig, containerOverride, resolvedProvider?.customEnv);
234
+ if (envLines.length > 0) {
235
+ const envFilePath = path.join(envDir, 'env');
236
+ const quotedLines = shellQuoteEnvLines(envLines);
237
+ fs.writeFileSync(envFilePath, quotedLines.join('\n') + '\n', {
238
+ mode: 0o600,
239
+ });
240
+ try {
241
+ fs.chmodSync(envFilePath, 0o600);
242
+ }
243
+ catch (err) {
244
+ logger.warn({ group: group.name, err }, 'Failed to enforce env file permissions');
245
+ }
246
+ mounts.push({
247
+ hostPath: envDir,
248
+ containerPath: '/workspace/env-dir',
249
+ readonly: true,
250
+ });
251
+ }
252
+ // Write .credentials.json for OAuth credentials (session dir is already mounted)
253
+ const mergedConfig = mergeClaudeEnvConfig(globalConfig, containerOverride);
254
+ if (mergedConfig.claudeOAuthCredentials) {
255
+ try {
256
+ writeCredentialsFile(groupSessionsDir, mergedConfig);
257
+ }
258
+ catch (err) {
259
+ logger.warn({ group: group.name, err }, 'Failed to write .credentials.json');
260
+ }
261
+ }
262
+ // Mount agent-runner source from host — recompiled on container startup.
263
+ // Bypasses Docker 镜像构建缓存,确保代码变更生效。
264
+ const agentRunnerSrc = path.join(packageRoot, 'container', 'agent-runner', 'src');
265
+ mounts.push({
266
+ hostPath: agentRunnerSrc,
267
+ containerPath: '/app/src',
268
+ readonly: true,
269
+ });
270
+ // External Claude runtime contract: keep ~/.claude/CLAUDE.md and rules/
271
+ // mounted into /workspace/ so the SDK's directory traversal (cwd → root)
272
+ // can still discover upstream Claude config files.
273
+ // Only for admin-created workspaces (ownerHomeFolder === 'main').
274
+ const isCreatorAdmin = ownerHomeFolder === 'main';
275
+ if (isCreatorAdmin) {
276
+ const hostClaudeDir = path.join(os.homedir(), '.claude');
277
+ const hostClaudeMd = path.join(hostClaudeDir, 'CLAUDE.md');
278
+ const hostRulesDir = path.join(hostClaudeDir, 'rules');
279
+ if (fs.existsSync(hostClaudeMd)) {
280
+ mounts.push({
281
+ hostPath: hostClaudeMd,
282
+ containerPath: '/workspace/CLAUDE.md',
283
+ readonly: true,
284
+ });
285
+ }
286
+ if (fs.existsSync(hostRulesDir)) {
287
+ mounts.push({
288
+ hostPath: hostRulesDir,
289
+ containerPath: '/workspace/.claude/rules',
290
+ readonly: true,
291
+ });
292
+ }
293
+ }
294
+ // Additional mounts validated against external allowlist (tamper-proof from containers)
295
+ if (group.containerConfig?.additionalMounts) {
296
+ const validatedMounts = validateAdditionalMounts(group.containerConfig.additionalMounts, group.name, isAdminHome);
297
+ mounts.push(...validatedMounts);
298
+ }
299
+ return mounts;
300
+ }
301
+ function buildContainerArgs(mounts, containerName) {
302
+ const args = ['run', '-i', '--rm', '--name', containerName];
303
+ // Docker: -v with :ro suffix for readonly
304
+ for (const mount of mounts) {
305
+ if (mount.readonly) {
306
+ args.push('-v', `${mount.hostPath}:${mount.containerPath}:ro`);
307
+ }
308
+ else {
309
+ args.push('-v', `${mount.hostPath}:${mount.containerPath}`);
310
+ }
311
+ }
312
+ args.push(CONTAINER_IMAGE);
313
+ return args;
314
+ }
315
+ export async function runContainerAgent(group, input, onProcess, onOutput, ownerHomeFolder) {
316
+ if ((group.agentType ?? 'claude') === 'codex') {
317
+ return {
318
+ status: 'error',
319
+ result: null,
320
+ error: 'Codex only supports host execution mode',
321
+ };
322
+ }
323
+ const startTime = Date.now();
324
+ const agentType = input.agentType || group.agentType || 'claude';
325
+ const selectedRunner = 'claude';
326
+ const runtimeBuildLogFields = getRuntimeBuildLogFields();
327
+ const groupDir = path.join(GROUPS_DIR, group.folder);
328
+ mkdirForContainer(groupDir);
329
+ // ─── Provider Pool selection ───
330
+ const poolResult = trySelectPoolProvider(group.folder);
331
+ const selectedProfileId = poolResult?.profileId ?? null;
332
+ const resolvedProvider = poolResult?.resolved;
333
+ try {
334
+ // Determine if this is an admin home container (full privileges)
335
+ const isAdminHome = !!group.is_home && group.folder === 'main';
336
+ // Per-user skills: always mount if the group has an owner
337
+ const shouldMountUserSkills = !!group.created_by;
338
+ const mounts = buildVolumeMounts(group, isAdminHome, shouldMountUserSkills, input.agentId, ownerHomeFolder, input.taskRunId, resolvedProvider);
339
+ const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-');
340
+ const agentSuffix = input.agentId
341
+ ? `-${input.agentId.replace(/[^a-zA-Z0-9-]/g, '-')}`
342
+ : '';
343
+ const containerName = `cli-claw-${safeName}${agentSuffix}-${Date.now()}`;
344
+ const containerArgs = buildContainerArgs(mounts, containerName);
345
+ logger.debug({
346
+ group: group.name,
347
+ containerName,
348
+ mounts: mounts.map((m) => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`),
349
+ containerArgs: containerArgs.join(' '),
350
+ }, 'Container mount configuration');
351
+ logger.info({
352
+ requestedAgentType: input.agentType || group.agentType || 'claude',
353
+ effectiveAgentType: agentType,
354
+ group: group.name,
355
+ folder: group.folder,
356
+ chatJid: input.chatJid,
357
+ agentType,
358
+ selectedRunner,
359
+ executionMode: 'container',
360
+ sessionId: input.sessionId || null,
361
+ agentId: input.agentId || null,
362
+ containerName,
363
+ mountCount: mounts.length,
364
+ isMain: input.isMain,
365
+ ...runtimeBuildLogFields,
366
+ }, 'Spawning container agent');
367
+ const logsDir = path.join(GROUPS_DIR, group.folder, 'logs');
368
+ fs.mkdirSync(logsDir, { recursive: true });
369
+ const result = await new Promise((resolve) => {
370
+ const container = spawn('docker', containerArgs, {
371
+ stdio: ['pipe', 'pipe', 'pipe'],
372
+ });
373
+ onProcess(container, containerName);
374
+ const stdoutState = createStdoutParserState();
375
+ const stderrState = createStderrState();
376
+ // Write input and close stdin (容器需要 EOF 来刷新 stdin 管道)
377
+ container.stdin.on('error', (err) => {
378
+ logger.error({ group: group.name, err }, 'Container stdin write failed');
379
+ container.kill();
380
+ });
381
+ container.stdin.write(JSON.stringify(input));
382
+ container.stdin.end();
383
+ let timedOut = false;
384
+ const timeoutMs = group.containerConfig?.timeout || getSystemSettings().containerTimeout;
385
+ const killOnTimeout = () => {
386
+ timedOut = true;
387
+ logger.error({ group: group.name, containerName }, 'Container timeout, stopping gracefully');
388
+ execFile('docker', ['stop', containerName], { timeout: 15000 }, (err) => {
389
+ if (err) {
390
+ logger.warn({ group: group.name, containerName, err }, 'Graceful stop failed, force killing');
391
+ container.kill('SIGKILL');
392
+ }
393
+ });
394
+ };
395
+ let timeout = setTimeout(killOnTimeout, timeoutMs);
396
+ const resetTimeout = () => {
397
+ clearTimeout(timeout);
398
+ timeout = setTimeout(killOnTimeout, timeoutMs);
399
+ };
400
+ // Attach stdout/stderr handlers using shared parser
401
+ attachStdoutHandler(container.stdout, stdoutState, {
402
+ groupName: group.name,
403
+ label: 'Container',
404
+ onOutput,
405
+ resetTimeout,
406
+ });
407
+ attachStderrHandler(container.stderr, stderrState, group.name, {
408
+ container: group.folder,
409
+ });
410
+ container.on('close', (code, signal) => {
411
+ clearTimeout(timeout);
412
+ const duration = Date.now() - startTime;
413
+ const closeCtx = {
414
+ groupName: group.name,
415
+ label: 'Container',
416
+ filePrefix: 'container',
417
+ identifier: containerName,
418
+ logsDir,
419
+ input: {
420
+ ...input,
421
+ agentType,
422
+ executionMode: 'container',
423
+ agentId: input.agentId,
424
+ },
425
+ stdoutState,
426
+ stderrState,
427
+ onOutput,
428
+ resolvePromise: resolve,
429
+ startTime,
430
+ timeoutMs,
431
+ agentIdentity: {
432
+ chatJid: input.chatJid,
433
+ groupFolder: group.folder,
434
+ agentType,
435
+ executionMode: 'container',
436
+ selectedRunner,
437
+ agentId: input.agentId || null,
438
+ },
439
+ runtimeBuildInfo: runtimeBuildLogFields,
440
+ extraSummaryLines: [
441
+ ``,
442
+ `=== Mounts ===`,
443
+ mounts
444
+ .map((m) => `${m.containerPath}${m.readonly ? ' (ro)' : ''}`)
445
+ .join('\n'),
446
+ ],
447
+ extraVerboseLines: [
448
+ `=== Container Args ===`,
449
+ containerArgs.join(' '),
450
+ ``,
451
+ `=== Mounts (detailed) ===`,
452
+ mounts
453
+ .map((m) => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`)
454
+ .join('\n'),
455
+ ],
456
+ };
457
+ if (handleTimeoutClose(closeCtx, code, duration, timedOut))
458
+ return;
459
+ const logFile = writeRunLog(closeCtx, code, duration);
460
+ if (handleNonZeroExit(closeCtx, code, signal, duration, logFile))
461
+ return;
462
+ handleSuccessClose(closeCtx, duration);
463
+ });
464
+ container.on('error', (err) => {
465
+ clearTimeout(timeout);
466
+ logger.error({ group: group.name, containerName, error: err }, 'Container spawn error');
467
+ resolve({
468
+ status: 'error',
469
+ result: null,
470
+ error: `Container spawn error: ${err.message}`,
471
+ });
472
+ });
473
+ });
474
+ // ─── Provider Pool health reporting ───
475
+ if (selectedProfileId) {
476
+ if (result.status === 'success' || result.status === 'closed') {
477
+ providerPool.reportSuccess(selectedProfileId);
478
+ }
479
+ else if (result.status === 'error' && isApiError(result.error || '')) {
480
+ providerPool.reportFailure(selectedProfileId);
481
+ }
482
+ }
483
+ return result;
484
+ }
485
+ finally {
486
+ // Guarantee session release even if buildVolumeMounts/spawn throws
487
+ if (selectedProfileId) {
488
+ providerPool.releaseSession(selectedProfileId);
489
+ }
490
+ }
491
+ }
492
+ export function writeTasksSnapshot(groupFolder, isAdminHome, tasks) {
493
+ // Write filtered tasks to the group's IPC directory
494
+ const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
495
+ fs.mkdirSync(groupIpcDir, { recursive: true });
496
+ // Admin home sees all tasks, others only see their own
497
+ const filteredTasks = isAdminHome
498
+ ? tasks
499
+ : tasks.filter((t) => t.groupFolder === groupFolder);
500
+ const tasksFile = path.join(groupIpcDir, 'current_tasks.json');
501
+ // 删除后重建:容器创建的文件归属 node(1000) 用户,宿主机进程无法覆写
502
+ try {
503
+ fs.unlinkSync(tasksFile);
504
+ }
505
+ catch {
506
+ /* ignore */
507
+ }
508
+ fs.writeFileSync(tasksFile, JSON.stringify(filteredTasks, null, 2));
509
+ }
510
+ /**
511
+ * Write available groups snapshot for the container to read.
512
+ * Only admin home can see all available groups (for activation).
513
+ * Other groups see nothing (they can't activate groups).
514
+ */
515
+ export function writeGroupsSnapshot(groupFolder, isAdminHome, groups, registeredJids) {
516
+ const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
517
+ fs.mkdirSync(groupIpcDir, { recursive: true });
518
+ // Admin home sees all groups; others see nothing (they can't activate groups)
519
+ const visibleGroups = isAdminHome ? groups : [];
520
+ const groupsFile = path.join(groupIpcDir, 'available_groups.json');
521
+ try {
522
+ fs.unlinkSync(groupsFile);
523
+ }
524
+ catch {
525
+ /* ignore */
526
+ }
527
+ fs.writeFileSync(groupsFile, JSON.stringify({
528
+ groups: visibleGroups,
529
+ lastSync: new Date().toISOString(),
530
+ }, null, 2));
531
+ }
532
+ /**
533
+ * 杀死进程及其所有子进程。
534
+ * 如果进程以 detached 模式启动(独立进程组),使用负 PID 杀整个进程组。
535
+ */
536
+ export function killProcessTree(proc, signal = 'SIGTERM') {
537
+ try {
538
+ if (proc.pid) {
539
+ process.kill(-proc.pid, signal);
540
+ return true;
541
+ }
542
+ }
543
+ catch {
544
+ try {
545
+ proc.kill(signal);
546
+ return true;
547
+ }
548
+ catch {
549
+ return false;
550
+ }
551
+ }
552
+ return false;
553
+ }
554
+ /**
555
+ * Run agent directly on the host machine (no Docker container).
556
+ * Used for host execution mode — the agent gets full access to the host filesystem.
557
+ */
558
+ export async function runHostAgent(group, input, onProcess, onOutput, ownerHomeFolder, options) {
559
+ const startTime = Date.now();
560
+ const setupInstallHint = 'npm --prefix container/agent-runner install';
561
+ const setupBuildHint = 'npm --prefix container/agent-runner run build';
562
+ const hostModeSetupError = (message) => ({
563
+ status: 'error',
564
+ result: `宿主机模式启动失败:${message}`,
565
+ error: message,
566
+ });
567
+ // 1. 确定存储目录与实际执行目录
568
+ const storageGroupDir = path.join(GROUPS_DIR, group.folder);
569
+ fs.mkdirSync(storageGroupDir, { recursive: true });
570
+ const initialExecutionCwd = options?.executionCwd ||
571
+ (group.executionMode === 'host' ? group.customCwd : storageGroupDir);
572
+ if (!initialExecutionCwd) {
573
+ return hostModeSetupError('Host workspace is missing custom_cwd. Run cli-claw start from the target directory or set custom_cwd explicitly.');
574
+ }
575
+ if (!path.isAbsolute(initialExecutionCwd)) {
576
+ return hostModeSetupError(`工作目录必须是绝对路径:${initialExecutionCwd}`);
577
+ }
578
+ // Resolve symlinks to prevent TOCTOU attacks
579
+ let groupDir = initialExecutionCwd;
580
+ try {
581
+ groupDir = fs.realpathSync(groupDir);
582
+ }
583
+ catch {
584
+ return hostModeSetupError(`工作目录不存在或无法解析:${groupDir}`);
585
+ }
586
+ if (!fs.statSync(groupDir).isDirectory()) {
587
+ return hostModeSetupError(`工作目录不是目录:${groupDir}`);
588
+ }
589
+ // Runtime allowlist validation for host CWD (defense-in-depth: web.ts validates at creation,
590
+ // but re-check here in case allowlist was tightened or path was injected via DB)
591
+ if (group.executionMode === 'host') {
592
+ const allowlist = loadMountAllowlist();
593
+ if (allowlist &&
594
+ allowlist.allowedRoots &&
595
+ allowlist.allowedRoots.length > 0) {
596
+ let allowed = false;
597
+ for (const root of allowlist.allowedRoots) {
598
+ const expandedRoot = root.path.startsWith('~')
599
+ ? path.join(process.env.HOME || '/Users/user', root.path.slice(root.path.startsWith('~/') ? 2 : 1))
600
+ : path.resolve(root.path);
601
+ let realRoot;
602
+ try {
603
+ realRoot = fs.realpathSync(expandedRoot);
604
+ }
605
+ catch {
606
+ continue;
607
+ }
608
+ const relative = path.relative(realRoot, groupDir);
609
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
610
+ allowed = true;
611
+ break;
612
+ }
613
+ }
614
+ if (!allowed) {
615
+ return hostModeSetupError(`工作目录 ${groupDir} 不在允许的根目录下,请检查 mount-allowlist.json`);
616
+ }
617
+ }
618
+ }
619
+ fs.mkdirSync(path.join(storageGroupDir, 'logs'), { recursive: true });
620
+ fs.mkdirSync(path.join(DATA_DIR, 'memory', group.folder), {
621
+ recursive: true,
622
+ });
623
+ // 2. 确保目录结构(宿主机模式下限制目录权限)
624
+ // Sub-agents get their own IPC and session directories
625
+ // Isolated tasks get their own IPC subdirectory under tasks-run/{taskRunId}/
626
+ const groupIpcDir = input.agentId
627
+ ? path.join(DATA_DIR, 'ipc', group.folder, 'agents', input.agentId)
628
+ : input.taskRunId
629
+ ? path.join(DATA_DIR, 'ipc', group.folder, 'tasks-run', input.taskRunId)
630
+ : path.join(DATA_DIR, 'ipc', group.folder);
631
+ fs.mkdirSync(path.join(groupIpcDir, 'messages'), {
632
+ recursive: true,
633
+ mode: 0o700,
634
+ });
635
+ fs.mkdirSync(path.join(groupIpcDir, 'tasks'), {
636
+ recursive: true,
637
+ mode: 0o700,
638
+ });
639
+ fs.mkdirSync(path.join(groupIpcDir, 'input'), {
640
+ recursive: true,
641
+ mode: 0o700,
642
+ });
643
+ // All agents (main + sub/conversation) get agents/ subdir for spawn/message IPC
644
+ fs.mkdirSync(path.join(groupIpcDir, 'agents'), {
645
+ recursive: true,
646
+ mode: 0o700,
647
+ });
648
+ const groupSessionsDir = input.agentId
649
+ ? path.join(DATA_DIR, 'sessions', group.folder, 'agents', input.agentId, '.claude')
650
+ : path.join(DATA_DIR, 'sessions', group.folder, '.claude');
651
+ fs.mkdirSync(groupSessionsDir, { recursive: true });
652
+ // 3. 写入 settings.json(合并模式,不覆盖已有用户配置)
653
+ // Load user's global MCP servers (same logic as Docker mode).
654
+ const settingsFile = path.join(groupSessionsDir, 'settings.json');
655
+ const hostMcpServers = group.created_by
656
+ ? loadUserMcpServers(group.created_by)
657
+ : {};
658
+ ensureSettingsJson(settingsFile, hostMcpServers);
659
+ // 4. Skills 自动链接到 session 目录
660
+ // 链接顺序:项目级 → 用户级(覆盖同名项目级)
661
+ // 用户的所有 skills 在所有工作区中生效
662
+ try {
663
+ const skillsDir = path.join(groupSessionsDir, 'skills');
664
+ fs.mkdirSync(skillsDir, { recursive: true });
665
+ // 清空已有符号链接
666
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
667
+ const entryPath = path.join(skillsDir, entry.name);
668
+ try {
669
+ if (entry.isSymbolicLink() || entry.isDirectory()) {
670
+ fs.rmSync(entryPath, { recursive: true, force: true });
671
+ }
672
+ }
673
+ catch {
674
+ /* ignore */
675
+ }
676
+ }
677
+ const linkSkillEntries = (sourceDir) => {
678
+ if (!fs.existsSync(sourceDir))
679
+ return;
680
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
681
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
682
+ continue;
683
+ const linkPath = path.join(skillsDir, entry.name);
684
+ try {
685
+ // 移除已有符号链接(高优先级覆盖低优先级)
686
+ if (fs.existsSync(linkPath)) {
687
+ fs.rmSync(linkPath, { recursive: true, force: true });
688
+ }
689
+ fs.symlinkSync(path.join(sourceDir, entry.name), linkPath);
690
+ }
691
+ catch {
692
+ /* ignore */
693
+ }
694
+ }
695
+ };
696
+ // 项目级 skills
697
+ linkSkillEntries(resolveAppPath('container', 'skills'));
698
+ // 用户级 skills(覆盖同名项目级)
699
+ const ownerId = group.created_by;
700
+ if (ownerId) {
701
+ linkSkillEntries(path.join(DATA_DIR, 'skills', ownerId));
702
+ }
703
+ }
704
+ catch (err) {
705
+ logger.warn({ folder: group.folder, err }, '宿主机模式 skills 符号链接失败');
706
+ }
707
+ // 5. 构建环境变量
708
+ const hostEnv = {
709
+ ...process.env,
710
+ };
711
+ const agentType = group.agentType ?? 'claude';
712
+ const selectedRunner = agentType;
713
+ const runtimeBuildLogFields = getRuntimeBuildLogFields();
714
+ // ─── Provider Pool selection (host mode) ───
715
+ const containerOverride = getContainerEnvConfig(group.folder);
716
+ const hostPoolResult = agentType === 'claude' ? trySelectPoolProvider(group.folder) : null;
717
+ const hostSelectedProfileId = hostPoolResult?.profileId ?? null;
718
+ const globalConfig = agentType === 'claude'
719
+ ? (hostPoolResult?.resolved.config ?? getClaudeProviderConfig())
720
+ : null;
721
+ try {
722
+ if (agentType === 'claude' && globalConfig) {
723
+ // 配置层环境变量
724
+ const envLines = buildContainerEnvLines(globalConfig, containerOverride, hostPoolResult?.resolved.customEnv);
725
+ for (const line of envLines) {
726
+ const eqIdx = line.indexOf('=');
727
+ if (eqIdx > 0) {
728
+ hostEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
729
+ }
730
+ }
731
+ // Write .credentials.json for OAuth credentials
732
+ const mergedConfig = mergeClaudeEnvConfig(globalConfig, containerOverride);
733
+ if (mergedConfig.claudeOAuthCredentials) {
734
+ try {
735
+ writeCredentialsFile(groupSessionsDir, mergedConfig);
736
+ }
737
+ catch (err) {
738
+ logger.warn({ folder: group.folder, err }, 'Failed to write .credentials.json for host agent');
739
+ }
740
+ }
741
+ }
742
+ // 路径映射
743
+ hostEnv['CLI_CLAW_WORKSPACE_GROUP'] = groupDir;
744
+ // Per-user global memory
745
+ const ownerId = requireWorkspaceOwner(group, 'host runtime');
746
+ const userGlobalDir = path.join(GROUPS_DIR, 'user-global', ownerId);
747
+ fs.mkdirSync(userGlobalDir, { recursive: true });
748
+ hostEnv['CLI_CLAW_WORKSPACE_GLOBAL'] = userGlobalDir;
749
+ const memoryFolder = group.is_home
750
+ ? group.folder
751
+ : ownerHomeFolder || group.folder;
752
+ hostEnv['CLI_CLAW_WORKSPACE_MEMORY'] = path.join(DATA_DIR, 'memory', memoryFolder);
753
+ hostEnv['CLI_CLAW_WORKSPACE_IPC'] = groupIpcDir;
754
+ if (agentType === 'claude') {
755
+ hostEnv['CLAUDE_CONFIG_DIR'] = groupSessionsDir;
756
+ // 让 SDK 捕获 CLI 的 stderr 输出,便于排查启动失败
757
+ hostEnv['DEBUG_CLAUDE_AGENT_SDK'] = '1';
758
+ // CLI 禁止 root 用户使用 --dangerously-skip-permissions,
759
+ // 通过 IS_SANDBOX 标记告知 CLI 当前运行在受控环境中以绕过此限制
760
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
761
+ hostEnv['IS_SANDBOX'] = '1';
762
+ }
763
+ }
764
+ // 6. 编译检查
765
+ const agentRunnerRoot = resolveAppPath('container', 'agent-runner');
766
+ const agentRunnerManifestPath = path.join(agentRunnerRoot, 'package.json');
767
+ const agentRunnerDist = path.join(agentRunnerRoot, 'dist', 'index.js');
768
+ if (!fs.existsSync(agentRunnerManifestPath)) {
769
+ logger.error({ group: group.name, agentRunnerRoot }, 'Host agent preflight failed: packaged agent-runner manifest missing');
770
+ return hostModeSetupError('缺少 container/agent-runner 资源。当前安装不支持 packaged host-mode agent-runner,请改用源码仓库运行或补齐该目录后重试。');
771
+ }
772
+ const requiredDeps = agentType === 'claude'
773
+ ? ['@anthropic-ai/claude-agent-sdk']
774
+ : ['@agentclientprotocol/sdk'];
775
+ const agentRunnerRequire = createRequire(agentRunnerManifestPath);
776
+ const missingDeps = requiredDeps.filter((dep) => {
777
+ try {
778
+ agentRunnerRequire.resolve(`${dep}/package.json`);
779
+ return false;
780
+ }
781
+ catch {
782
+ return true;
783
+ }
784
+ });
785
+ if (missingDeps.length > 0) {
786
+ const missing = missingDeps.join(', ');
787
+ logger.error({ group: group.name, missingDeps }, 'Host agent preflight failed: dependencies missing');
788
+ return hostModeSetupError(`缺少 agent-runner 依赖(${missing})。请先执行:${setupInstallHint}`);
789
+ }
790
+ if (!fs.existsSync(agentRunnerDist)) {
791
+ logger.error({ group: group.name, agentRunnerDist }, 'Host agent preflight failed: dist not found');
792
+ return hostModeSetupError(`agent-runner 产物缺失。请先执行:${setupBuildHint};若这是安装包环境,请确认包含 container/agent-runner/dist。`);
793
+ }
794
+ if (agentType === 'codex') {
795
+ try {
796
+ execFileSync('codex', ['login', 'status'], {
797
+ stdio: 'ignore',
798
+ timeout: 10_000,
799
+ });
800
+ }
801
+ catch {
802
+ return hostModeSetupError('Codex CLI 未登录。请先在服务器上执行:codex login');
803
+ }
804
+ }
805
+ // Auto-rebuild if dist is stale (src newer than dist)
806
+ try {
807
+ const distMtime = fs.statSync(agentRunnerDist).mtimeMs;
808
+ const srcDir = path.join(agentRunnerRoot, 'src');
809
+ const srcFiles = fs.readdirSync(srcDir);
810
+ const newestSrc = Math.max(...srcFiles.map((f) => fs.statSync(path.join(srcDir, f)).mtimeMs));
811
+ if (newestSrc > distMtime) {
812
+ logger.info({ group: group.name }, 'agent-runner dist 已过期,自动重新编译...');
813
+ try {
814
+ const { execSync } = await import('child_process');
815
+ execSync('npm run build', {
816
+ cwd: agentRunnerRoot,
817
+ stdio: 'pipe',
818
+ timeout: 30_000,
819
+ });
820
+ logger.info({ group: group.name }, 'agent-runner 自动编译完成');
821
+ }
822
+ catch (buildErr) {
823
+ logger.warn({ group: group.name, err: buildErr }, `agent-runner 自动编译失败,使用旧版 dist。手动执行:${setupBuildHint}`);
824
+ }
825
+ }
826
+ }
827
+ catch {
828
+ // Best effort, don't block execution
829
+ }
830
+ logger.info({
831
+ requestedAgentType: input.agentType || group.agentType || 'claude',
832
+ effectiveAgentType: agentType,
833
+ group: group.name,
834
+ folder: group.folder,
835
+ chatJid: input.chatJid,
836
+ agentType,
837
+ selectedRunner,
838
+ executionMode: 'host',
839
+ sessionId: input.sessionId || null,
840
+ agentId: input.agentId || null,
841
+ workingDir: groupDir,
842
+ isMain: input.isMain,
843
+ ...runtimeBuildLogFields,
844
+ }, 'Spawning host agent');
845
+ const logsDir = path.join(storageGroupDir, 'logs');
846
+ const hostResult = await new Promise((resolve) => {
847
+ let settled = false;
848
+ const resolveOnce = (output) => {
849
+ if (settled)
850
+ return;
851
+ settled = true;
852
+ resolve(output);
853
+ };
854
+ // 7. 启动进程
855
+ const proc = spawn('node', [agentRunnerDist], {
856
+ stdio: ['pipe', 'pipe', 'pipe'],
857
+ env: hostEnv,
858
+ cwd: groupDir,
859
+ detached: true,
860
+ });
861
+ const processId = `host-${group.folder}-${Date.now()}`;
862
+ onProcess(proc, processId);
863
+ const stdoutState = createStdoutParserState();
864
+ const stderrState = createStderrState();
865
+ // 8. stdin 输入
866
+ proc.stdin.on('error', (err) => {
867
+ logger.error({ group: group.name, err }, 'Host agent stdin write failed');
868
+ killProcessTree(proc);
869
+ });
870
+ proc.stdin.write(JSON.stringify(input));
871
+ proc.stdin.end();
872
+ // 9. 超时管理
873
+ let timedOut = false;
874
+ const timeoutMs = group.containerConfig?.timeout || getSystemSettings().containerTimeout;
875
+ let killTimer = null;
876
+ const killOnTimeout = () => {
877
+ timedOut = true;
878
+ logger.error({ group: group.name, processId }, 'Host agent timeout, killing');
879
+ killProcessTree(proc, 'SIGTERM');
880
+ killTimer = setTimeout(() => {
881
+ if (proc.exitCode === null && proc.signalCode === null) {
882
+ killProcessTree(proc, 'SIGKILL');
883
+ }
884
+ }, 5000);
885
+ };
886
+ let timeout = setTimeout(killOnTimeout, timeoutMs);
887
+ const resetTimeout = () => {
888
+ clearTimeout(timeout);
889
+ timeout = setTimeout(killOnTimeout, timeoutMs);
890
+ };
891
+ // 10. stdout/stderr 解析
892
+ attachStdoutHandler(proc.stdout, stdoutState, {
893
+ groupName: group.name,
894
+ label: 'Host agent',
895
+ onOutput,
896
+ resetTimeout,
897
+ });
898
+ attachStderrHandler(proc.stderr, stderrState, group.name, {
899
+ host: group.folder,
900
+ });
901
+ // 11. close 事件处理
902
+ proc.on('close', (code, signal) => {
903
+ clearTimeout(timeout);
904
+ if (killTimer)
905
+ clearTimeout(killTimer);
906
+ const duration = Date.now() - startTime;
907
+ const closeCtx = {
908
+ groupName: group.name,
909
+ label: 'Host Agent',
910
+ filePrefix: 'host',
911
+ identifier: processId,
912
+ logsDir,
913
+ input: {
914
+ ...input,
915
+ agentType,
916
+ executionMode: 'host',
917
+ agentId: input.agentId,
918
+ },
919
+ stdoutState,
920
+ stderrState,
921
+ onOutput,
922
+ resolvePromise: resolveOnce,
923
+ startTime,
924
+ timeoutMs,
925
+ agentIdentity: {
926
+ chatJid: input.chatJid,
927
+ groupFolder: group.folder,
928
+ agentType,
929
+ executionMode: 'host',
930
+ selectedRunner,
931
+ agentId: input.agentId || null,
932
+ },
933
+ runtimeBuildInfo: runtimeBuildLogFields,
934
+ extraSummaryLines: [`Working Directory: ${groupDir}`],
935
+ enrichError: (stderrContent, exitLabel) => {
936
+ const missingPackageMatch = stderrContent.match(/Cannot find package '([^']+)' imported from/u);
937
+ const userFacingError = (missingPackageMatch
938
+ ? `宿主机模式启动失败:缺少依赖 ${missingPackageMatch[1]}。请先执行:${setupInstallHint}`
939
+ : null) || formatUserFacingRuntimeError(stderrContent);
940
+ return {
941
+ result: userFacingError,
942
+ error: `Host agent exited with ${exitLabel}: ${stderrContent.slice(-200)}`,
943
+ };
944
+ },
945
+ };
946
+ if (handleTimeoutClose(closeCtx, code, duration, timedOut))
947
+ return;
948
+ const logFile = writeRunLog(closeCtx, code, duration);
949
+ if (handleNonZeroExit(closeCtx, code, signal, duration, logFile))
950
+ return;
951
+ handleSuccessClose(closeCtx, duration);
952
+ });
953
+ proc.on('error', (err) => {
954
+ clearTimeout(timeout);
955
+ logger.error({ group: group.name, processId, error: err }, 'Host agent spawn error');
956
+ resolveOnce({
957
+ status: 'error',
958
+ result: null,
959
+ error: `Host agent spawn error: ${err.message}`,
960
+ });
961
+ });
962
+ });
963
+ // ─── Provider Pool health reporting (host mode) ───
964
+ if (agentType === 'claude' && hostSelectedProfileId) {
965
+ if (hostResult.status === 'success' || hostResult.status === 'closed') {
966
+ providerPool.reportSuccess(hostSelectedProfileId);
967
+ }
968
+ else if (hostResult.status === 'error' &&
969
+ isApiError(hostResult.error || '')) {
970
+ providerPool.reportFailure(hostSelectedProfileId);
971
+ }
972
+ }
973
+ return hostResult;
974
+ }
975
+ finally {
976
+ // Guarantee session release even if spawn/setup throws
977
+ if (agentType === 'claude' && hostSelectedProfileId) {
978
+ providerPool.releaseSession(hostSelectedProfileId);
979
+ }
980
+ }
981
+ }