onbuzz 4.9.13 → 4.10.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 (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
@@ -1,1081 +1,1081 @@
1
- /**
2
- * PlatformControlTool — agent-facing control over Loxia platform features.
3
- *
4
- * Designed as a single tool that grows by ADDING sub-features (each with
5
- * its own permission key) rather than spawning new tools. Today's surface:
6
- * - Scheduled tasks (CRUD + trigger + self-resume convenience)
7
- *
8
- * Permission model — per-agent, per-feature, three levels:
9
- * - 'disabled' (default): the agent cannot use the feature
10
- * - 'own': only this agent's own agent-target schedules
11
- * - 'all': all agent-target schedules (admin scope)
12
- *
13
- * Flow-target schedules are NEVER reachable here, regardless of level —
14
- * agents do not (currently) control flows. See permissions.js for the
15
- * security boundary; THIS file just dispatches actions.
16
- *
17
- * Per-agent config shape:
18
- * agent.toolConfig.platformcontrol = {
19
- * scheduledTasks: 'disabled' | 'own' | 'all', // default 'disabled'
20
- * // future features get their own keys here
21
- * }
22
- */
23
-
24
- import { BaseTool } from './baseTool.js';
25
- import {
26
- SCHEDULE_ACCESS_LEVELS,
27
- getScheduleAccessLevel,
28
- checkScheduleAccess,
29
- filterAccessibleSchedules,
30
- AGENT_ACCESS_LEVELS,
31
- getAgentAccessLevel,
32
- getMaxAgentsCreated,
33
- checkAgentMutationAccess,
34
- clampToolConfigForChild,
35
- getTeamScope,
36
- isTeamAccessDisabled,
37
- checkTeamAccess,
38
- filterAccessibleTeams,
39
- FLOW_ACCESS_LEVELS,
40
- getFlowAccessLevel,
41
- getMaxFlowsCreated,
42
- checkFlowMutationAccess,
43
- } from './platformControl/permissions.js';
44
- import { isProtectedFromCaller, makeAgentLookup } from './platformControl/ancestry.js';
45
- import { cascadeDeleteAgent, cascadeDeleteTeam } from './platformControl/cascadeService.js';
46
-
47
- const SUPPORTED_ACTIONS = [
48
- 'list-capabilities',
49
- // Scheduled tasks
50
- 'list-schedules',
51
- 'get-schedule',
52
- 'create-schedule',
53
- 'update-schedule',
54
- 'delete-schedule',
55
- 'toggle-schedule',
56
- 'trigger-schedule',
57
- 'list-presets',
58
- // Convenience: one-shot self-resume schedule (TODO: rethink — currently
59
- // a thin wrapper around create-schedule with runOnce + cron derived from
60
- // a future ISO datetime. May become a first-class `wakeAt` primitive
61
- // once we see how agents actually use it.)
62
- 'schedule-self-resume',
63
- // Agent CRUD
64
- 'list-agents',
65
- 'create-agent',
66
- 'update-agent',
67
- 'delete-agent',
68
- // Team CRUD + membership
69
- 'list-teams',
70
- 'create-team',
71
- 'update-team',
72
- 'delete-team',
73
- 'add-team-member',
74
- 'remove-team-member',
75
- // Flow CRUD + execution. Permission key: `flows`. Mirrors agents.
76
- 'list-flows',
77
- 'get-flow',
78
- 'create-flow',
79
- 'update-flow',
80
- 'delete-flow',
81
- 'execute-flow',
82
- 'dry-run-flow',
83
- ];
84
-
85
- /**
86
- * Convert an ISO datetime in the FUTURE into a one-shot cron expression.
87
- * Cron resolution is one minute, so we round down to the matching minute.
88
- * Returns the cron string and a normalized ISO so the caller can echo it.
89
- *
90
- * Throws on invalid input or past times — this is user-visible feedback,
91
- * not a silent fallback.
92
- */
93
- function isoToOneShotCron(runAt) {
94
- if (typeof runAt !== 'string' || !runAt.trim()) {
95
- throw new Error('runAt must be an ISO datetime string');
96
- }
97
- const d = new Date(runAt);
98
- if (Number.isNaN(d.getTime())) {
99
- throw new Error(`runAt is not a valid datetime: "${runAt}"`);
100
- }
101
- // Round down to the minute — cron resolution.
102
- d.setSeconds(0, 0);
103
- if (d.getTime() <= Date.now()) {
104
- throw new Error('runAt must be at least one minute in the future');
105
- }
106
- const cron = `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${d.getMonth() + 1} *`;
107
- return { cron, normalizedIso: d.toISOString() };
108
- }
109
-
110
- class PlatformControlTool extends BaseTool {
111
- constructor(config = {}, logger = null) {
112
- super(config, logger);
113
- this.id = 'platformcontrol';
114
- this.requiresProject = false;
115
- this.isAsync = false;
116
-
117
- /** @type {object|null} */ this.scheduleService = null;
118
- /** @type {object|null} */ this.agentPool = null;
119
- /** @type {object|null} */ this.stateManager = null;
120
- /** @type {object|null} */ this.memoryService = null;
121
- /** @type {object|null} */ this.flowExecutor = null;
122
- }
123
-
124
- /** Injected from src/index.js after ScheduleService is initialized. */
125
- setScheduleService(scheduleService) {
126
- this.scheduleService = scheduleService;
127
- this.logger?.info?.(`[platformcontrol] ScheduleService ${scheduleService ? 'attached' : 'detached'}`);
128
- }
129
-
130
- /** Injected — needed for agent CRUD + ancestry walks. */
131
- setAgentPool(agentPool) {
132
- this.agentPool = agentPool;
133
- this.logger?.info?.(`[platformcontrol] AgentPool ${agentPool ? 'attached' : 'detached'}`);
134
- }
135
-
136
- /** Injected — needed for team CRUD + membership cascade cleanup. */
137
- setStateManager(stateManager) {
138
- this.stateManager = stateManager;
139
- this.logger?.info?.(`[platformcontrol] StateManager ${stateManager ? 'attached' : 'detached'}`);
140
- }
141
-
142
- /** Injected — needed for memory cleanup on agent deletion. */
143
- setMemoryService(memoryService) {
144
- this.memoryService = memoryService;
145
- this.logger?.info?.(`[platformcontrol] MemoryService ${memoryService ? 'attached' : 'detached'}`);
146
- }
147
-
148
- /** Injected — needed for execute-flow / dry-run-flow actions. */
149
- setFlowExecutor(flowExecutor) {
150
- this.flowExecutor = flowExecutor;
151
- this.logger?.info?.(`[platformcontrol] FlowExecutor ${flowExecutor ? 'attached' : 'detached'}`);
152
- }
153
-
154
- getDescription() {
155
- return `
156
- Platform Control Tool: control Loxia platform features (scheduled tasks today; more later).
157
-
158
- Per-agent permission, default DISABLED. Configure in the tool configurator.
159
-
160
- ACTIONS — scheduled tasks:
161
- list-schedules — list schedules in scope
162
- get-schedule { scheduleId } — fetch one schedule
163
- create-schedule { name, prompt, cronExpression,
164
- targetAgentId?, enabled?,
165
- runOnce?, maxRuns?,
166
- startDate?, endDate?, description? }
167
- — create. targetAgentId defaults to self in 'own' mode.
168
- Pass 'self' to explicitly target this agent.
169
- update-schedule { scheduleId, ...updates } — patch fields (name, prompt, cron, enabled, etc.)
170
- delete-schedule { scheduleId } — remove
171
- toggle-schedule { scheduleId, enabled? } — flip enabled (or set if provided)
172
- trigger-schedule { scheduleId } — fire NOW, out of band
173
- list-presets — read-only list of cron presets
174
- schedule-self-resume { runAt, prompt, name? } — one-shot schedule that wakes THIS agent
175
- at runAt (ISO datetime, future, minute-resolution)
176
- with the given prompt. TODO: under review.
177
-
178
- ACTIONS — agents (per-agent permission: 'disabled' | 'self-created' | 'all'; default 'disabled'):
179
- list-agents — list all agents (read is unrestricted at any non-disabled level)
180
- create-agent { name, systemPrompt,
181
- description?, model?, capabilities?,
182
- skills?, toolConfig?,
183
- directoryAccess? } — create a new agent. createdBy is set to YOU automatically.
184
- Per-creator quota (maxAgentsCreated, default unlimited)
185
- applies. Child's toolConfig.platformcontrol.* is CLAMPED
186
- to your level (no privilege escalation).
187
- update-agent { agentId, ...fields } — patch fields. Same clamp rule on toolConfig.
188
- Hard rules: cannot target self; cannot target ancestors.
189
- delete-agent { agentId } — cascade-delete: schedules, memories, team memberships,
190
- then the agent itself.
191
-
192
- ACTIONS — teams (per-agent multi-select scope: member / ownedByMe / all; default all-false = disabled):
193
- list-teams — list teams reachable in your scope
194
- create-team { name, description?, color? } — create. createdBy is set to YOU automatically.
195
- update-team { teamId, name?, description?, color? }
196
- delete-team { teamId } — removes the team; member assignments die with it.
197
- add-team-member { teamId, agentId } — add an agent to a team. agentId='self' adds caller.
198
- remove-team-member { teamId, agentId } — remove an agent. agentId='self' = leave the team.
199
-
200
- ACTIONS — flows (per-agent permission: 'disabled' | 'self-created' | 'all'; default 'disabled'):
201
- list-flows — list all flows reachable in your scope (read is unrestricted at any non-disabled level)
202
- get-flow { flowId } — fetch a flow's full definition (nodes + edges + variables)
203
- create-flow { name, description?, nodes, edges?, variables? }
204
- — create a new flow. Definition must pass schema validation.
205
- createdBy is set to YOU automatically.
206
- Per-creator quota (maxFlowsCreated, default unlimited) applies.
207
- update-flow { flowId, ...fields } — patch a flow you have permission to mutate.
208
- The full re-validated definition is required when changing nodes/edges.
209
- delete-flow { flowId } — cascade-delete: run history + checkpoints + version snapshots.
210
- execute-flow { flowId, input? } — kick off a run. Returns runId for polling.
211
- dry-run-flow { flowId } OR { flow } — lint-only check without execution; surfaces structural issues
212
- (orphaned nodes, missing required fields, etc.) before live runs.
213
-
214
- list-capabilities — what permission level + scope this agent has
215
-
216
- EXAMPLES:
217
-
218
- # Wake me up tomorrow at 9am with a check-in prompt
219
- {
220
- "toolId": "platformcontrol",
221
- "action": "schedule-self-resume",
222
- "runAt": "2026-04-27T09:00:00Z",
223
- "prompt": "Resume task: review yesterday's PR comments and respond.",
224
- "name": "Morning PR review"
225
- }
226
-
227
- # Recurring agent-self schedule
228
- {
229
- "toolId": "platformcontrol",
230
- "action": "create-schedule",
231
- "name": "Hourly health check",
232
- "prompt": "Run smoke tests and report status.",
233
- "cronExpression": "0 * * * *",
234
- "targetAgentId": "self"
235
- }
236
-
237
- # List my own schedules
238
- { "toolId": "platformcontrol", "action": "list-schedules" }
239
-
240
- NOTES:
241
- - Flow schedules are NEVER reachable from this tool, regardless of permission level.
242
- - 'self' is a sugar for the calling agent's id; explicit ids work too.
243
- - cronExpression accepts the standard 5-field cron OR a preset name (call list-presets).
244
- `;
245
- }
246
-
247
- getSupportedActions() { return [...SUPPORTED_ACTIONS]; }
248
-
249
- async execute(params, context = {}) {
250
- if (!params || typeof params !== 'object') {
251
- return { success: false, error: 'params must be an object' };
252
- }
253
- // Reverse-forgiveness: accept plural {actions:[{...}]} envelope.
254
- if (Array.isArray(params?.actions) && params.actions.length > 0) {
255
- const a = params.actions[0] || {};
256
- params = { ...params, ...a, action: a.type || a.action || params.action };
257
- }
258
- const action = params.action || 'list-capabilities';
259
- const callerAgentId = context?.agentId || null;
260
-
261
- const cfg = this.getEffectiveConfig(context, {});
262
- const level = getScheduleAccessLevel(cfg);
263
-
264
- // list-capabilities is always allowed — agent needs to discover its
265
- // own permissions even when scheduledTasks is 'disabled'.
266
- if (action === 'list-capabilities') {
267
- return this._listCapabilities(level, cfg);
268
- }
269
-
270
- // Per-feature gating: each action's permission lives in its own slice
271
- // of toolConfig.platformcontrol. Schedule actions get scheduleLevel
272
- // gating; agent / team actions handle their own gates inside their
273
- // handlers (which need the full cfg anyway for the privilege clamp).
274
- const isScheduleAction = action.includes('schedule') || action === 'list-presets';
275
- if (isScheduleAction) {
276
- if (level === SCHEDULE_ACCESS_LEVELS.DISABLED) {
277
- return {
278
- success: false,
279
- disabled: true,
280
- error: 'Scheduled-tasks access is disabled for this agent. Enable it in the platformcontrol tool configurator.',
281
- };
282
- }
283
- if (!this.scheduleService) {
284
- return { success: false, error: 'ScheduleService is not available on this server.' };
285
- }
286
- }
287
-
288
- // IMPORTANT: `await` inside the try so async-handler rejections are
289
- // caught here. Without await, a `return this._createSchedule(...)`
290
- // hands an un-awaited Promise back; rejections propagate past this
291
- // try/catch and surface as raw exceptions to the caller.
292
- try {
293
- switch (action) {
294
- case 'list-schedules': return await this._listSchedules(level, callerAgentId);
295
- case 'get-schedule': return await this._getSchedule(level, callerAgentId, params);
296
- case 'create-schedule': return await this._createSchedule(level, callerAgentId, params);
297
- case 'update-schedule': return await this._updateSchedule(level, callerAgentId, params);
298
- case 'delete-schedule': return await this._deleteSchedule(level, callerAgentId, params);
299
- case 'toggle-schedule': return await this._toggleSchedule(level, callerAgentId, params);
300
- case 'trigger-schedule': return await this._triggerSchedule(level, callerAgentId, params);
301
- case 'list-presets': return await this._listPresets();
302
- case 'schedule-self-resume': return await this._scheduleSelfResume(level, callerAgentId, params);
303
- // Agent CRUD
304
- case 'list-agents': return await this._listAgents(cfg, callerAgentId);
305
- case 'create-agent': return await this._createAgent(cfg, callerAgentId, params);
306
- case 'update-agent': return await this._updateAgent(cfg, callerAgentId, params);
307
- case 'delete-agent': return await this._deleteAgent(cfg, callerAgentId, params);
308
- // Team CRUD + membership
309
- case 'list-teams': return await this._listTeams(cfg, callerAgentId);
310
- case 'create-team': return await this._createTeam(cfg, callerAgentId, params);
311
- case 'update-team': return await this._updateTeam(cfg, callerAgentId, params);
312
- case 'delete-team': return await this._deleteTeam(cfg, callerAgentId, params);
313
- case 'add-team-member': return await this._addTeamMember(cfg, callerAgentId, params);
314
- case 'remove-team-member': return await this._removeTeamMember(cfg, callerAgentId, params);
315
- // Flow CRUD + execution
316
- case 'list-flows': return await this._listFlows(cfg, callerAgentId);
317
- case 'get-flow': return await this._getFlow(cfg, callerAgentId, params);
318
- case 'create-flow': return await this._createFlow(cfg, callerAgentId, params);
319
- case 'update-flow': return await this._updateFlow(cfg, callerAgentId, params);
320
- case 'delete-flow': return await this._deleteFlow(cfg, callerAgentId, params);
321
- case 'execute-flow': return await this._executeFlow(cfg, callerAgentId, params);
322
- case 'dry-run-flow': return await this._dryRunFlow(cfg, callerAgentId, params);
323
- default:
324
- return {
325
- success: false,
326
- error: `Unknown action: ${action}. Supported: ${SUPPORTED_ACTIONS.join(', ')}`,
327
- };
328
- }
329
- } catch (err) {
330
- this.logger?.error?.('[platformcontrol] action failed', { action, error: err?.message });
331
- return { success: false, error: err?.message || 'unknown error' };
332
- }
333
- }
334
-
335
- _listCapabilities(level, fullCfg) {
336
- const agentLevel = getAgentAccessLevel(fullCfg);
337
- const teamScope = getTeamScope(fullCfg);
338
- const teamsDisabled = isTeamAccessDisabled(fullCfg);
339
- return {
340
- success: true,
341
- action: 'list-capabilities',
342
- capabilities: {
343
- scheduledTasks: {
344
- level,
345
- canListOwn: level !== SCHEDULE_ACCESS_LEVELS.DISABLED,
346
- canListAll: level === SCHEDULE_ACCESS_LEVELS.ALL,
347
- canMutateOwn: level !== SCHEDULE_ACCESS_LEVELS.DISABLED,
348
- canMutateAll: level === SCHEDULE_ACCESS_LEVELS.ALL,
349
- notes: [
350
- 'Flow-target schedules are not reachable from this tool.',
351
- 'Default level is "disabled". Configure via the platformcontrol tool configurator.',
352
- ],
353
- },
354
- agents: {
355
- level: agentLevel,
356
- maxAgentsCreated: getMaxAgentsCreated(fullCfg),
357
- canList: agentLevel !== AGENT_ACCESS_LEVELS.DISABLED,
358
- canCreate: agentLevel !== AGENT_ACCESS_LEVELS.DISABLED,
359
- canMutateSelfCreated: agentLevel !== AGENT_ACCESS_LEVELS.DISABLED,
360
- canMutateAll: agentLevel === AGENT_ACCESS_LEVELS.ALL,
361
- notes: [
362
- 'Hard rule: an agent cannot modify itself or any of its ancestors.',
363
- 'When configuring another agent\'s toolConfig, child permissions are clamped to your level.',
364
- 'maxAgentsCreated null = unlimited; counted only against agents YOU created via this tool.',
365
- ],
366
- },
367
- teams: {
368
- scope: teamScope,
369
- disabled: teamsDisabled,
370
- notes: [
371
- 'Multi-select scope: you can act on teams you are a member of, teams you created, or all (each independently).',
372
- 'Leaving a team you\'re a member of is allowed.',
373
- ],
374
- },
375
- flows: {
376
- level: getFlowAccessLevel(fullCfg),
377
- maxFlowsCreated: getMaxFlowsCreated(fullCfg),
378
- canList: getFlowAccessLevel(fullCfg) !== FLOW_ACCESS_LEVELS.DISABLED,
379
- canCreate: getFlowAccessLevel(fullCfg) !== FLOW_ACCESS_LEVELS.DISABLED,
380
- canMutateSelfCreated: getFlowAccessLevel(fullCfg) !== FLOW_ACCESS_LEVELS.DISABLED,
381
- canMutateAll: getFlowAccessLevel(fullCfg) === FLOW_ACCESS_LEVELS.ALL,
382
- notes: [
383
- 'Permission key: flows. Default level is "disabled".',
384
- 'Flow definitions are validated against the v2 schema before save — invalid flows are rejected with detailed errors.',
385
- 'delete-flow cascades through run history, checkpoints, and version snapshots.',
386
- 'execute-flow respects existing flow permission levels — at "self-created" level you can only run flows you created.',
387
- ],
388
- },
389
- },
390
- };
391
- }
392
-
393
- _listSchedules(level, callerAgentId) {
394
- const all = this.scheduleService.listSchedules();
395
- const accessible = filterAccessibleSchedules(level, callerAgentId, all);
396
- return {
397
- success: true,
398
- action: 'list-schedules',
399
- count: accessible.length,
400
- schedules: accessible,
401
- scope: level,
402
- };
403
- }
404
-
405
- _getSchedule(level, callerAgentId, params) {
406
- const id = params?.scheduleId;
407
- if (!id) return { success: false, error: 'scheduleId is required' };
408
- const schedule = this.scheduleService.getSchedule(id);
409
- if (!schedule) return { success: false, error: `Schedule not found: ${id}` };
410
- const access = checkScheduleAccess(level, callerAgentId, schedule);
411
- if (!access.allow) {
412
- // Surface as not-found rather than scope-denied so 'own' agents
413
- // cannot probe for the existence of other agents' schedules by id.
414
- return { success: false, error: `Schedule not found: ${id}` };
415
- }
416
- return { success: true, action: 'get-schedule', schedule };
417
- }
418
-
419
- /**
420
- * Resolve `targetAgentId` from the create/self-resume params:
421
- * - 'self' → callerAgentId
422
- * - undefined → callerAgentId (default to self)
423
- * - any string → the explicit id
424
- *
425
- * In 'own' mode we then assert the resolved id MUST equal callerAgentId.
426
- */
427
- _resolveTargetAgentId(level, callerAgentId, raw) {
428
- if (raw === 'self' || raw === undefined || raw === null || raw === '') return callerAgentId;
429
- if (typeof raw !== 'string') {
430
- throw new Error('targetAgentId must be a string (or "self")');
431
- }
432
- if (level === SCHEDULE_ACCESS_LEVELS.OWN && raw !== callerAgentId) {
433
- throw new Error(
434
- `Out of scope: 'own' mode requires targetAgentId === this agent's id ("${callerAgentId}"). ` +
435
- `Set the platformcontrol tool to 'all' for cross-agent scheduling.`
436
- );
437
- }
438
- return raw;
439
- }
440
-
441
- async _createSchedule(level, callerAgentId, params) {
442
- const targetAgentId = this._resolveTargetAgentId(level, callerAgentId, params?.targetAgentId);
443
- const built = {
444
- name: params?.name,
445
- prompt: params?.prompt,
446
- targetType: 'agent', // tool only ever creates agent-target schedules
447
- targetId: targetAgentId,
448
- cronExpression: params?.cronExpression,
449
- enabled: params?.enabled !== false, // default true
450
- description: params?.description || '',
451
- startDate: params?.startDate || null,
452
- endDate: params?.endDate || null,
453
- maxRuns: params?.maxRuns ?? null,
454
- runOnce: !!params?.runOnce,
455
- };
456
- const schedule = await this.scheduleService.createSchedule(built);
457
- return { success: true, action: 'create-schedule', schedule };
458
- }
459
-
460
- async _updateSchedule(level, callerAgentId, params) {
461
- const id = params?.scheduleId;
462
- if (!id) return { success: false, error: 'scheduleId is required' };
463
- const existing = this.scheduleService.getSchedule(id);
464
- if (!existing) return { success: false, error: `Schedule not found: ${id}` };
465
- const access = checkScheduleAccess(level, callerAgentId, existing);
466
- if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
467
-
468
- // 'own' mode cannot reassign a schedule to a different target.
469
- if (params?.targetAgentId !== undefined && params.targetAgentId !== null) {
470
- const desired = this._resolveTargetAgentId(level, callerAgentId, params.targetAgentId);
471
- if (level === SCHEDULE_ACCESS_LEVELS.OWN && desired !== callerAgentId) {
472
- return {
473
- success: false,
474
- error: `Out of scope: 'own' mode cannot reassign schedule "${id}" to another agent.`,
475
- };
476
- }
477
- }
478
-
479
- const updates = {};
480
- const PASS = ['name', 'description', 'prompt', 'cronExpression', 'enabled', 'startDate', 'endDate', 'maxRuns', 'runOnce'];
481
- for (const k of PASS) if (params[k] !== undefined) updates[k] = params[k];
482
- if (params?.targetAgentId !== undefined && params.targetAgentId !== null) {
483
- updates.targetId = this._resolveTargetAgentId(level, callerAgentId, params.targetAgentId);
484
- }
485
- const schedule = await this.scheduleService.updateSchedule(id, updates);
486
- return { success: true, action: 'update-schedule', schedule };
487
- }
488
-
489
- async _deleteSchedule(level, callerAgentId, params) {
490
- const id = params?.scheduleId;
491
- if (!id) return { success: false, error: 'scheduleId is required' };
492
- const existing = this.scheduleService.getSchedule(id);
493
- if (!existing) return { success: false, error: `Schedule not found: ${id}` };
494
- const access = checkScheduleAccess(level, callerAgentId, existing);
495
- if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
496
- await this.scheduleService.deleteSchedule(id);
497
- return { success: true, action: 'delete-schedule', scheduleId: id };
498
- }
499
-
500
- async _toggleSchedule(level, callerAgentId, params) {
501
- const id = params?.scheduleId;
502
- if (!id) return { success: false, error: 'scheduleId is required' };
503
- const existing = this.scheduleService.getSchedule(id);
504
- if (!existing) return { success: false, error: `Schedule not found: ${id}` };
505
- const access = checkScheduleAccess(level, callerAgentId, existing);
506
- if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
507
-
508
- const next = typeof params?.enabled === 'boolean' ? params.enabled : !existing.enabled;
509
- const schedule = await this.scheduleService.updateSchedule(id, { enabled: next });
510
- return { success: true, action: 'toggle-schedule', scheduleId: id, enabled: schedule.enabled };
511
- }
512
-
513
- async _triggerSchedule(level, callerAgentId, params) {
514
- const id = params?.scheduleId;
515
- if (!id) return { success: false, error: 'scheduleId is required' };
516
- const existing = this.scheduleService.getSchedule(id);
517
- if (!existing) return { success: false, error: `Schedule not found: ${id}` };
518
- const access = checkScheduleAccess(level, callerAgentId, existing);
519
- if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
520
-
521
- // ScheduleService doesn't expose a public "trigger now" API yet; the
522
- // closest is _executeSchedule which is internal. Use the private path
523
- // when available; otherwise mark this action as not-implemented so the
524
- // agent gets clear feedback rather than a silent stub.
525
- if (typeof this.scheduleService._executeSchedule === 'function') {
526
- // Fire-and-forget — execution may take a while, the agent shouldn't block.
527
- this.scheduleService._executeSchedule(existing).catch(err => {
528
- this.logger?.warn?.('[platformcontrol] trigger-schedule execution failed', {
529
- scheduleId: id, error: err?.message,
530
- });
531
- });
532
- return { success: true, action: 'trigger-schedule', scheduleId: id, triggered: true };
533
- }
534
- return {
535
- success: false,
536
- error: 'trigger-schedule is not supported on this version of ScheduleService.',
537
- };
538
- }
539
-
540
- _listPresets() {
541
- // Presets live in scheduleService; expose via a stable surface.
542
- // CRON_PRESETS isn't exported, so we surface the names plus a note
543
- // pointing to scheduleService.listSchedules's cronPreset field.
544
- // (TODO: export presets from scheduleService and read them here.)
545
- return {
546
- success: true,
547
- action: 'list-presets',
548
- presets: [
549
- 'every-minute', 'every-5-minutes', 'every-15-minutes', 'every-30-minutes',
550
- 'every-hour', 'every-6-hours', 'every-12-hours',
551
- 'daily', 'daily-morning', 'daily-evening',
552
- 'weekdays', 'weekends', 'weekly-monday', 'monthly',
553
- ],
554
- note: 'Pass a preset name as cronExpression OR a raw 5-field cron string.',
555
- };
556
- }
557
-
558
- async _scheduleSelfResume(level, callerAgentId, params) {
559
- // TODO: rethink ergonomics. Currently a thin wrapper around
560
- // create-schedule with runOnce + cron derived from a future ISO
561
- // datetime. May become a first-class wake-at primitive after we
562
- // see how agents actually plan around it.
563
- if (!callerAgentId) {
564
- return { success: false, error: 'callerAgentId is required (no context.agentId)' };
565
- }
566
- const { runAt, prompt, name } = params || {};
567
- if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
568
- return { success: false, error: 'prompt (non-empty string) is required' };
569
- }
570
- let cron, normalizedIso;
571
- try {
572
- ({ cron, normalizedIso } = isoToOneShotCron(runAt));
573
- } catch (err) {
574
- return { success: false, error: err.message };
575
- }
576
- const schedule = await this.scheduleService.createSchedule({
577
- name: name || `Self-resume @ ${normalizedIso}`,
578
- description: 'One-shot self-resume created via platformcontrol.schedule-self-resume',
579
- prompt,
580
- targetType: 'agent',
581
- targetId: callerAgentId,
582
- cronExpression: cron,
583
- runOnce: true,
584
- enabled: true,
585
- });
586
- return {
587
- success: true,
588
- action: 'schedule-self-resume',
589
- runAt: normalizedIso,
590
- cronExpression: cron,
591
- schedule,
592
- };
593
- }
594
-
595
- // ─── Agent + team helpers ─────────────────────────────────────────
596
-
597
- /**
598
- * Snapshot all agents into a Map keyed by id, used to build the
599
- * `getAgent` lookup the ancestry walker needs. Done once per action
600
- * so a single call doesn't hit the agent pool repeatedly.
601
- *
602
- * Returns a tuple { agents, getAgent, isProtectedFromCaller }.
603
- * isProtectedFromCaller is curried with the lookup so it can be
604
- * passed into checkAgentMutationAccess as opts.isProtectedFromCaller.
605
- */
606
- async _agentSnapshot(callerAgentId) {
607
- if (!this.agentPool || typeof this.agentPool.getAllAgents !== 'function') {
608
- return { agents: [], getAgent: () => null, isProtectedFromCaller: () => false };
609
- }
610
- const all = await this.agentPool.getAllAgents();
611
- // Normalize to array (getAllAgents may return a Map).
612
- const arr = (all instanceof Map) ? Array.from(all.values()) : Array.isArray(all) ? all : [];
613
- const getAgent = makeAgentLookup(arr);
614
- const protect = (callerId, targetId) => isProtectedFromCaller(callerId, targetId, getAgent);
615
- return { agents: arr, getAgent, isProtectedFromCaller: protect };
616
- }
617
-
618
- /** Project an agent into the safe summary shape we expose to other agents. */
619
- _projectAgent(a) {
620
- if (!a) return null;
621
- return {
622
- id: a.id,
623
- name: a.name,
624
- mode: a.mode,
625
- status: a.status,
626
- currentModel: a.currentModel,
627
- preferredModel: a.preferredModel || null,
628
- capabilities: Array.isArray(a.capabilities) ? a.capabilities.slice() : [],
629
- skills: Array.isArray(a.skills) ? a.skills.slice() : [],
630
- createdBy: a.createdBy || null,
631
- createdAt: a.createdAt || null,
632
- };
633
- }
634
-
635
- // ─── Agent CRUD ───────────────────────────────────────────────────
636
-
637
- async _listAgents(cfg, callerAgentId) {
638
- const level = getAgentAccessLevel(cfg);
639
- if (level === AGENT_ACCESS_LEVELS.DISABLED) {
640
- return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
641
- }
642
- if (!this.agentPool) return { success: false, error: 'AgentPool unavailable.' };
643
- const { agents } = await this._agentSnapshot(callerAgentId);
644
- // Read is unrestricted at any non-disabled level — agents need to
645
- // enumerate to make decisions. Mutations are gated separately.
646
- const list = agents.map(a => this._projectAgent(a));
647
- return { success: true, action: 'list-agents', count: list.length, agents: list, scope: level };
648
- }
649
-
650
- async _createAgent(cfg, callerAgentId, params) {
651
- const level = getAgentAccessLevel(cfg);
652
- if (level === AGENT_ACCESS_LEVELS.DISABLED) {
653
- return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
654
- }
655
- if (!this.agentPool || typeof this.agentPool.createAgent !== 'function') {
656
- return { success: false, error: 'AgentPool unavailable.' };
657
- }
658
- const { name, systemPrompt, model } = params || {};
659
- if (!name || typeof name !== 'string' || !name.trim()) {
660
- return { success: false, error: 'name (non-empty string) is required' };
661
- }
662
- if (!systemPrompt || typeof systemPrompt !== 'string') {
663
- return { success: false, error: 'systemPrompt (string) is required' };
664
- }
665
-
666
- // Per-creator quota — count alive agents whose createdBy === caller.
667
- const max = getMaxAgentsCreated(cfg);
668
- if (max !== null) {
669
- const { agents } = await this._agentSnapshot(callerAgentId);
670
- const owned = agents.filter(a => a.createdBy === callerAgentId).length;
671
- if (owned >= max) {
672
- return {
673
- success: false,
674
- error: `Per-creator agent quota exhausted (${owned}/${max}). Delete one of your created agents or ask the user to raise maxAgentsCreated.`,
675
- };
676
- }
677
- }
678
-
679
- // Privilege clamp on the requested toolConfig.platformcontrol.
680
- let pcSliceClamps = [];
681
- let nextToolConfig = (params.toolConfig && typeof params.toolConfig === 'object') ? { ...params.toolConfig } : {};
682
- if (nextToolConfig.platformcontrol && typeof nextToolConfig.platformcontrol === 'object') {
683
- const callerPc = (cfg && typeof cfg === 'object') ? cfg : {};
684
- const { config: clampedPc, clamps } = clampToolConfigForChild(callerPc, nextToolConfig.platformcontrol);
685
- nextToolConfig.platformcontrol = clampedPc;
686
- pcSliceClamps = clamps;
687
- }
688
-
689
- const newConfig = {
690
- name: name.trim(),
691
- description: typeof params.description === 'string' ? params.description : '',
692
- systemPrompt,
693
- model: model || undefined,
694
- preferredModel: params.preferredModel || model || undefined,
695
- capabilities: Array.isArray(params.capabilities) ? params.capabilities.slice() : [],
696
- skills: Array.isArray(params.skills) ? params.skills.slice() : [],
697
- toolConfig: nextToolConfig,
698
- directoryAccess: params.directoryAccess || undefined,
699
- // KEY: tag the new agent's parent so ancestry rules apply forever after.
700
- createdBy: callerAgentId,
701
- };
702
-
703
- const created = await this.agentPool.createAgent(newConfig);
704
- return {
705
- success: true,
706
- action: 'create-agent',
707
- agent: this._projectAgent(created),
708
- clamps: pcSliceClamps,
709
- };
710
- }
711
-
712
- async _updateAgent(cfg, callerAgentId, params) {
713
- const level = getAgentAccessLevel(cfg);
714
- if (level === AGENT_ACCESS_LEVELS.DISABLED) {
715
- return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
716
- }
717
- if (!this.agentPool) return { success: false, error: 'AgentPool unavailable.' };
718
- const targetId = params?.agentId;
719
- if (!targetId) return { success: false, error: 'agentId is required' };
720
-
721
- const { agents, isProtectedFromCaller: protect } = await this._agentSnapshot(callerAgentId);
722
- const target = agents.find(a => a.id === targetId);
723
- if (!target) return { success: false, error: `Agent not found: ${targetId}` };
724
-
725
- const access = checkAgentMutationAccess(level, callerAgentId, target, {
726
- isProtectedFromCaller: protect,
727
- });
728
- if (!access.allow) {
729
- return { success: false, error: access.reason };
730
- }
731
-
732
- // Build the updates patch — everything the user UI accepts, with the
733
- // privilege clamp applied to toolConfig.platformcontrol.
734
- const updates = {};
735
- const PASS_THROUGH = ['name', 'description', 'systemPrompt', 'model', 'preferredModel',
736
- 'capabilities', 'skills', 'directoryAccess'];
737
- for (const k of PASS_THROUGH) if (params[k] !== undefined) updates[k] = params[k];
738
-
739
- let pcSliceClamps = [];
740
- if (params.toolConfig && typeof params.toolConfig === 'object') {
741
- const next = { ...params.toolConfig };
742
- if (next.platformcontrol && typeof next.platformcontrol === 'object') {
743
- const callerPc = (cfg && typeof cfg === 'object') ? cfg : {};
744
- const { config: clampedPc, clamps } = clampToolConfigForChild(callerPc, next.platformcontrol);
745
- next.platformcontrol = clampedPc;
746
- pcSliceClamps = clamps;
747
- }
748
- updates.toolConfig = next;
749
- }
750
-
751
- const updated = await this.agentPool.updateAgent(targetId, updates);
752
- return {
753
- success: true,
754
- action: 'update-agent',
755
- agent: this._projectAgent(updated || target),
756
- clamps: pcSliceClamps,
757
- };
758
- }
759
-
760
- async _deleteAgent(cfg, callerAgentId, params) {
761
- const level = getAgentAccessLevel(cfg);
762
- if (level === AGENT_ACCESS_LEVELS.DISABLED) {
763
- return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
764
- }
765
- if (!this.agentPool) return { success: false, error: 'AgentPool unavailable.' };
766
- const targetId = params?.agentId;
767
- if (!targetId) return { success: false, error: 'agentId is required' };
768
-
769
- const { agents, isProtectedFromCaller: protect } = await this._agentSnapshot(callerAgentId);
770
- const target = agents.find(a => a.id === targetId);
771
- if (!target) return { success: false, error: `Agent not found: ${targetId}` };
772
-
773
- const access = checkAgentMutationAccess(level, callerAgentId, target, {
774
- isProtectedFromCaller: protect,
775
- });
776
- if (!access.allow) return { success: false, error: access.reason };
777
-
778
- const report = await cascadeDeleteAgent({
779
- agentId: targetId,
780
- scheduleService: this.scheduleService,
781
- memoryService: this.memoryService,
782
- stateManager: this.stateManager,
783
- agentPool: this.agentPool,
784
- logger: this.logger,
785
- });
786
- return {
787
- success: report.agentDeleted,
788
- action: 'delete-agent',
789
- agentId: targetId,
790
- report,
791
- };
792
- }
793
-
794
- // ─── Team CRUD + membership ───────────────────────────────────────
795
-
796
- async _listTeams(cfg, callerAgentId) {
797
- if (isTeamAccessDisabled(cfg)) {
798
- return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
799
- }
800
- if (!this.stateManager || typeof this.stateManager.getAllTeams !== 'function') {
801
- return { success: false, error: 'StateManager unavailable.' };
802
- }
803
- const all = await this.stateManager.getAllTeams();
804
- const scope = getTeamScope(cfg);
805
- // Per the design: read is unrestricted within scope (not unlimited
806
- // like agents). The agent only sees teams it's reachable for —
807
- // listing teams it can never act on adds noise without value.
808
- const accessible = filterAccessibleTeams(scope, callerAgentId, all);
809
- return { success: true, action: 'list-teams', count: accessible.length, teams: accessible, scope };
810
- }
811
-
812
- async _createTeam(cfg, callerAgentId, params) {
813
- if (isTeamAccessDisabled(cfg)) {
814
- return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
815
- }
816
- if (!this.stateManager || typeof this.stateManager.createTeam !== 'function') {
817
- return { success: false, error: 'StateManager unavailable.' };
818
- }
819
- const { name, description, color } = params || {};
820
- if (!name || typeof name !== 'string' || !name.trim()) {
821
- return { success: false, error: 'name (non-empty string) is required' };
822
- }
823
- const team = await this.stateManager.createTeam({
824
- name: name.trim(),
825
- description: description || '',
826
- color,
827
- createdBy: callerAgentId,
828
- });
829
- return { success: true, action: 'create-team', team };
830
- }
831
-
832
- /**
833
- * Verify scope access on an EXISTING team. Returns the team or an
834
- * error result — the caller propagates either.
835
- */
836
- async _resolveTeamForAccess(cfg, callerAgentId, teamId) {
837
- if (!teamId) return { error: 'teamId is required' };
838
- const team = await this.stateManager.getTeam(teamId);
839
- if (!team) return { error: `Team not found: ${teamId}` };
840
- const scope = getTeamScope(cfg);
841
- const access = checkTeamAccess(scope, callerAgentId, team);
842
- if (!access.allow) return { error: `Team not found: ${teamId}` }; // hide existence
843
- return { team };
844
- }
845
-
846
- async _updateTeam(cfg, callerAgentId, params) {
847
- if (isTeamAccessDisabled(cfg)) {
848
- return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
849
- }
850
- const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
851
- if (got.error) return { success: false, error: got.error };
852
- const updates = {};
853
- for (const k of ['name', 'description', 'color']) {
854
- if (params[k] !== undefined) updates[k] = params[k];
855
- }
856
- const updated = await this.stateManager.updateTeam(params.teamId, updates);
857
- return { success: true, action: 'update-team', team: updated };
858
- }
859
-
860
- async _deleteTeam(cfg, callerAgentId, params) {
861
- if (isTeamAccessDisabled(cfg)) {
862
- return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
863
- }
864
- const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
865
- if (got.error) return { success: false, error: got.error };
866
- const report = await cascadeDeleteTeam({
867
- teamId: params.teamId,
868
- stateManager: this.stateManager,
869
- logger: this.logger,
870
- });
871
- return { success: report.teamDeleted, action: 'delete-team', teamId: params.teamId, report };
872
- }
873
-
874
- /**
875
- * Adding a member is a team mutation. Self-add is allowed iff the
876
- * caller's scope already covers the team (e.g. 'all', 'ownedByMe').
877
- * The hard rules apply to AGENT mutations — adding agentX to a team
878
- * is a TEAM mutation, not an agent mutation, so no ancestor check.
879
- */
880
- async _addTeamMember(cfg, callerAgentId, params) {
881
- if (isTeamAccessDisabled(cfg)) {
882
- return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
883
- }
884
- const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
885
- if (got.error) return { success: false, error: got.error };
886
- const agentId = params?.agentId === 'self' ? callerAgentId : params?.agentId;
887
- if (!agentId) return { success: false, error: 'agentId is required' };
888
- const team = await this.stateManager.addAgentToTeam(params.teamId, agentId);
889
- return { success: true, action: 'add-team-member', teamId: params.teamId, agentId, team };
890
- }
891
-
892
- /**
893
- * Removing a member is a team mutation. SPECIAL CASE: if the agent
894
- * is removing ITSELF (leaving), allow it as long as the team is
895
- * within scope — leaving a team is not self-modification of the
896
- * agent record, it's modification of the team's member list.
897
- * If the agent has 'member' scope and is leaving, the team IS in
898
- * scope by definition, so this just works.
899
- */
900
- async _removeTeamMember(cfg, callerAgentId, params) {
901
- if (isTeamAccessDisabled(cfg)) {
902
- return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
903
- }
904
- const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
905
- if (got.error) return { success: false, error: got.error };
906
- const agentId = params?.agentId === 'self' ? callerAgentId : params?.agentId;
907
- if (!agentId) return { success: false, error: 'agentId is required' };
908
- const team = await this.stateManager.removeAgentFromTeam(params.teamId, agentId);
909
- return { success: true, action: 'remove-team-member', teamId: params.teamId, agentId, team };
910
- }
911
-
912
- // ─── Flow CRUD + execution ─────────────────────────────────────────
913
- //
914
- // Permission key: `flows`. Default 'disabled'. Same shape as agents:
915
- // 'self-created' = only flows you authored, 'all' = admin scope.
916
- // Read endpoints (list/get) are unrestricted at any non-disabled
917
- // level so an agent can browse what's available before deciding which
918
- // to run; mutations (create/update/delete/execute/dry-run) all go
919
- // through `checkFlowMutationAccess`.
920
-
921
- /** Resolve the project dir the same way every other path here does. */
922
- _flowProjectDir() {
923
- return this.stateManager?.config?.project?.directory
924
- || this.stateManager?.config?.projectDir
925
- || process.cwd();
926
- }
927
-
928
- async _listFlows(cfg, callerAgentId) {
929
- const level = getFlowAccessLevel(cfg);
930
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
931
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
932
- }
933
- if (!this.stateManager) {
934
- return { success: false, error: 'StateManager not available.' };
935
- }
936
- const all = await this.stateManager.getAllFlows(this._flowProjectDir());
937
- // List is unrestricted at any non-disabled level — agents can see
938
- // what they could run / what the platform offers. Mutability is
939
- // surfaced per-row so the LLM knows what it can act on.
940
- const list = (all || []).map(f => ({
941
- id: f.id,
942
- name: f.name,
943
- description: f.description || '',
944
- version: f.version,
945
- createdBy: f.createdBy || null,
946
- nodeCount: Array.isArray(f.nodes) ? f.nodes.length : 0,
947
- mutable: checkFlowMutationAccess(level, callerAgentId, f).allow,
948
- }));
949
- return { success: true, action: 'list-flows', count: list.length, flows: list, scope: level };
950
- }
951
-
952
- async _getFlow(cfg, callerAgentId, params) {
953
- const level = getFlowAccessLevel(cfg);
954
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
955
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
956
- }
957
- if (!params?.flowId) return { success: false, error: 'flowId is required' };
958
- const flow = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
959
- if (!flow) return { success: false, error: `Flow not found: ${params.flowId}` };
960
- return {
961
- success: true, action: 'get-flow', flow,
962
- mutable: checkFlowMutationAccess(level, callerAgentId, flow).allow,
963
- };
964
- }
965
-
966
- async _createFlow(cfg, callerAgentId, params) {
967
- const level = getFlowAccessLevel(cfg);
968
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
969
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
970
- }
971
- if (!params?.name || typeof params.name !== 'string') {
972
- return { success: false, error: 'name is required' };
973
- }
974
- if (!Array.isArray(params.nodes) || params.nodes.length === 0) {
975
- return { success: false, error: 'nodes array is required (must have at least one node)' };
976
- }
977
- // Quota enforcement — count flows the caller has authored so far.
978
- const max = getMaxFlowsCreated(cfg);
979
- if (max !== null) {
980
- const owned = (await this.stateManager.getAllFlows(this._flowProjectDir()))
981
- .filter(f => f.createdBy === callerAgentId);
982
- if (owned.length >= max) {
983
- return {
984
- success: false,
985
- error: `maxFlowsCreated quota reached: you have created ${owned.length} of ${max} allowed flows.`,
986
- };
987
- }
988
- }
989
- // Schema validation runs inside stateManager.createFlow already
990
- // (via the route layer); we additionally call validate here so an
991
- // invalid create returns a clean structured error rather than going
992
- // through the route. Defensive — duplicate guard, no harm.
993
- const created = await this.stateManager.createFlow({
994
- name: params.name,
995
- description: params.description || '',
996
- nodes: params.nodes,
997
- edges: params.edges || [],
998
- variables: params.variables || {},
999
- // Stamp the caller as the author so self-created scope can find it.
1000
- createdBy: callerAgentId,
1001
- }, this._flowProjectDir());
1002
- return { success: true, action: 'create-flow', flow: created };
1003
- }
1004
-
1005
- async _updateFlow(cfg, callerAgentId, params) {
1006
- const level = getFlowAccessLevel(cfg);
1007
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1008
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1009
- }
1010
- if (!params?.flowId) return { success: false, error: 'flowId is required' };
1011
- const target = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1012
- if (!target) return { success: false, error: `Flow not found: ${params.flowId}` };
1013
- const access = checkFlowMutationAccess(level, callerAgentId, target);
1014
- if (!access.allow) return { success: false, error: access.reason };
1015
- // Strip identity-level fields the caller shouldn't be able to spoof.
1016
- const { id: _id, createdBy: _cb, version: _v, ...patch } = params;
1017
- const updated = await this.stateManager.updateFlow(params.flowId, patch, this._flowProjectDir());
1018
- return { success: true, action: 'update-flow', flow: updated };
1019
- }
1020
-
1021
- async _deleteFlow(cfg, callerAgentId, params) {
1022
- const level = getFlowAccessLevel(cfg);
1023
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1024
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1025
- }
1026
- if (!params?.flowId) return { success: false, error: 'flowId is required' };
1027
- const target = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1028
- if (!target) return { success: false, error: `Flow not found: ${params.flowId}` };
1029
- const access = checkFlowMutationAccess(level, callerAgentId, target);
1030
- if (!access.allow) return { success: false, error: access.reason };
1031
- await this.stateManager.deleteFlow(params.flowId, this._flowProjectDir());
1032
- return { success: true, action: 'delete-flow', flowId: params.flowId };
1033
- }
1034
-
1035
- async _executeFlow(cfg, callerAgentId, params) {
1036
- const level = getFlowAccessLevel(cfg);
1037
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1038
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1039
- }
1040
- if (!params?.flowId) return { success: false, error: 'flowId is required' };
1041
- if (!this.flowExecutor) return { success: false, error: 'FlowExecutor not available on this server.' };
1042
- const target = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1043
- if (!target) return { success: false, error: `Flow not found: ${params.flowId}` };
1044
- // Execute counts as a mutation (it consumes credits + writes run history).
1045
- const access = checkFlowMutationAccess(level, callerAgentId, target);
1046
- if (!access.allow) return { success: false, error: access.reason };
1047
- const run = await this.flowExecutor.executeFlow(params.flowId, params.input || {}, {
1048
- projectDir: this._flowProjectDir(),
1049
- // Tag the run with the agent that triggered it so audit trails
1050
- // attribute correctly when an agent kicks a flow off via the tool.
1051
- triggeredBy: { kind: 'agent', agentId: callerAgentId },
1052
- });
1053
- return { success: true, action: 'execute-flow', runId: run?.runId || run?.id || null, status: run?.status || 'queued' };
1054
- }
1055
-
1056
- async _dryRunFlow(cfg, callerAgentId, params) {
1057
- const level = getFlowAccessLevel(cfg);
1058
- if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1059
- return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1060
- }
1061
- // Dry-run accepts EITHER an existing flowId OR an inline flow def
1062
- // (so an agent can lint a draft before saving).
1063
- let flow;
1064
- if (params?.flowId) {
1065
- flow = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1066
- if (!flow) return { success: false, error: `Flow not found: ${params.flowId}` };
1067
- } else if (params?.flow && typeof params.flow === 'object') {
1068
- flow = params.flow;
1069
- } else {
1070
- return { success: false, error: 'Pass either { flowId } or { flow: {...} }' };
1071
- }
1072
- if (!this.flowExecutor || typeof this.flowExecutor.dryRun !== 'function') {
1073
- return { success: false, error: 'FlowExecutor.dryRun not available on this server.' };
1074
- }
1075
- const report = await this.flowExecutor.dryRun(flow);
1076
- return { success: true, action: 'dry-run-flow', report };
1077
- }
1078
- }
1079
-
1080
- export default PlatformControlTool;
1081
- export { PlatformControlTool };
1
+ /**
2
+ * PlatformControlTool — agent-facing control over Loxia platform features.
3
+ *
4
+ * Designed as a single tool that grows by ADDING sub-features (each with
5
+ * its own permission key) rather than spawning new tools. Today's surface:
6
+ * - Scheduled tasks (CRUD + trigger + self-resume convenience)
7
+ *
8
+ * Permission model — per-agent, per-feature, three levels:
9
+ * - 'disabled' (default): the agent cannot use the feature
10
+ * - 'own': only this agent's own agent-target schedules
11
+ * - 'all': all agent-target schedules (admin scope)
12
+ *
13
+ * Flow-target schedules are NEVER reachable here, regardless of level —
14
+ * agents do not (currently) control flows. See permissions.js for the
15
+ * security boundary; THIS file just dispatches actions.
16
+ *
17
+ * Per-agent config shape:
18
+ * agent.toolConfig.platformcontrol = {
19
+ * scheduledTasks: 'disabled' | 'own' | 'all', // default 'disabled'
20
+ * // future features get their own keys here
21
+ * }
22
+ */
23
+
24
+ import { BaseTool } from './baseTool.js';
25
+ import {
26
+ SCHEDULE_ACCESS_LEVELS,
27
+ getScheduleAccessLevel,
28
+ checkScheduleAccess,
29
+ filterAccessibleSchedules,
30
+ AGENT_ACCESS_LEVELS,
31
+ getAgentAccessLevel,
32
+ getMaxAgentsCreated,
33
+ checkAgentMutationAccess,
34
+ clampToolConfigForChild,
35
+ getTeamScope,
36
+ isTeamAccessDisabled,
37
+ checkTeamAccess,
38
+ filterAccessibleTeams,
39
+ FLOW_ACCESS_LEVELS,
40
+ getFlowAccessLevel,
41
+ getMaxFlowsCreated,
42
+ checkFlowMutationAccess,
43
+ } from './platformControl/permissions.js';
44
+ import { isProtectedFromCaller, makeAgentLookup } from './platformControl/ancestry.js';
45
+ import { cascadeDeleteAgent, cascadeDeleteTeam } from './platformControl/cascadeService.js';
46
+
47
+ const SUPPORTED_ACTIONS = [
48
+ 'list-capabilities',
49
+ // Scheduled tasks
50
+ 'list-schedules',
51
+ 'get-schedule',
52
+ 'create-schedule',
53
+ 'update-schedule',
54
+ 'delete-schedule',
55
+ 'toggle-schedule',
56
+ 'trigger-schedule',
57
+ 'list-presets',
58
+ // Convenience: one-shot self-resume schedule (TODO: rethink — currently
59
+ // a thin wrapper around create-schedule with runOnce + cron derived from
60
+ // a future ISO datetime. May become a first-class `wakeAt` primitive
61
+ // once we see how agents actually use it.)
62
+ 'schedule-self-resume',
63
+ // Agent CRUD
64
+ 'list-agents',
65
+ 'create-agent',
66
+ 'update-agent',
67
+ 'delete-agent',
68
+ // Team CRUD + membership
69
+ 'list-teams',
70
+ 'create-team',
71
+ 'update-team',
72
+ 'delete-team',
73
+ 'add-team-member',
74
+ 'remove-team-member',
75
+ // Flow CRUD + execution. Permission key: `flows`. Mirrors agents.
76
+ 'list-flows',
77
+ 'get-flow',
78
+ 'create-flow',
79
+ 'update-flow',
80
+ 'delete-flow',
81
+ 'execute-flow',
82
+ 'dry-run-flow',
83
+ ];
84
+
85
+ /**
86
+ * Convert an ISO datetime in the FUTURE into a one-shot cron expression.
87
+ * Cron resolution is one minute, so we round down to the matching minute.
88
+ * Returns the cron string and a normalized ISO so the caller can echo it.
89
+ *
90
+ * Throws on invalid input or past times — this is user-visible feedback,
91
+ * not a silent fallback.
92
+ */
93
+ function isoToOneShotCron(runAt) {
94
+ if (typeof runAt !== 'string' || !runAt.trim()) {
95
+ throw new Error('runAt must be an ISO datetime string');
96
+ }
97
+ const d = new Date(runAt);
98
+ if (Number.isNaN(d.getTime())) {
99
+ throw new Error(`runAt is not a valid datetime: "${runAt}"`);
100
+ }
101
+ // Round down to the minute — cron resolution.
102
+ d.setSeconds(0, 0);
103
+ if (d.getTime() <= Date.now()) {
104
+ throw new Error('runAt must be at least one minute in the future');
105
+ }
106
+ const cron = `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${d.getMonth() + 1} *`;
107
+ return { cron, normalizedIso: d.toISOString() };
108
+ }
109
+
110
+ class PlatformControlTool extends BaseTool {
111
+ constructor(config = {}, logger = null) {
112
+ super(config, logger);
113
+ this.id = 'platformcontrol';
114
+ this.requiresProject = false;
115
+ this.isAsync = false;
116
+
117
+ /** @type {object|null} */ this.scheduleService = null;
118
+ /** @type {object|null} */ this.agentPool = null;
119
+ /** @type {object|null} */ this.stateManager = null;
120
+ /** @type {object|null} */ this.memoryService = null;
121
+ /** @type {object|null} */ this.flowExecutor = null;
122
+ }
123
+
124
+ /** Injected from src/index.js after ScheduleService is initialized. */
125
+ setScheduleService(scheduleService) {
126
+ this.scheduleService = scheduleService;
127
+ this.logger?.info?.(`[platformcontrol] ScheduleService ${scheduleService ? 'attached' : 'detached'}`);
128
+ }
129
+
130
+ /** Injected — needed for agent CRUD + ancestry walks. */
131
+ setAgentPool(agentPool) {
132
+ this.agentPool = agentPool;
133
+ this.logger?.info?.(`[platformcontrol] AgentPool ${agentPool ? 'attached' : 'detached'}`);
134
+ }
135
+
136
+ /** Injected — needed for team CRUD + membership cascade cleanup. */
137
+ setStateManager(stateManager) {
138
+ this.stateManager = stateManager;
139
+ this.logger?.info?.(`[platformcontrol] StateManager ${stateManager ? 'attached' : 'detached'}`);
140
+ }
141
+
142
+ /** Injected — needed for memory cleanup on agent deletion. */
143
+ setMemoryService(memoryService) {
144
+ this.memoryService = memoryService;
145
+ this.logger?.info?.(`[platformcontrol] MemoryService ${memoryService ? 'attached' : 'detached'}`);
146
+ }
147
+
148
+ /** Injected — needed for execute-flow / dry-run-flow actions. */
149
+ setFlowExecutor(flowExecutor) {
150
+ this.flowExecutor = flowExecutor;
151
+ this.logger?.info?.(`[platformcontrol] FlowExecutor ${flowExecutor ? 'attached' : 'detached'}`);
152
+ }
153
+
154
+ getDescription() {
155
+ return `
156
+ Platform Control Tool: control Loxia platform features (scheduled tasks today; more later).
157
+
158
+ Per-agent permission, default DISABLED. Configure in the tool configurator.
159
+
160
+ ACTIONS — scheduled tasks:
161
+ list-schedules — list schedules in scope
162
+ get-schedule { scheduleId } — fetch one schedule
163
+ create-schedule { name, prompt, cronExpression,
164
+ targetAgentId?, enabled?,
165
+ runOnce?, maxRuns?,
166
+ startDate?, endDate?, description? }
167
+ — create. targetAgentId defaults to self in 'own' mode.
168
+ Pass 'self' to explicitly target this agent.
169
+ update-schedule { scheduleId, ...updates } — patch fields (name, prompt, cron, enabled, etc.)
170
+ delete-schedule { scheduleId } — remove
171
+ toggle-schedule { scheduleId, enabled? } — flip enabled (or set if provided)
172
+ trigger-schedule { scheduleId } — fire NOW, out of band
173
+ list-presets — read-only list of cron presets
174
+ schedule-self-resume { runAt, prompt, name? } — one-shot schedule that wakes THIS agent
175
+ at runAt (ISO datetime, future, minute-resolution)
176
+ with the given prompt. TODO: under review.
177
+
178
+ ACTIONS — agents (per-agent permission: 'disabled' | 'self-created' | 'all'; default 'disabled'):
179
+ list-agents — list all agents (read is unrestricted at any non-disabled level)
180
+ create-agent { name, systemPrompt,
181
+ description?, model?, capabilities?,
182
+ skills?, toolConfig?,
183
+ directoryAccess? } — create a new agent. createdBy is set to YOU automatically.
184
+ Per-creator quota (maxAgentsCreated, default unlimited)
185
+ applies. Child's toolConfig.platformcontrol.* is CLAMPED
186
+ to your level (no privilege escalation).
187
+ update-agent { agentId, ...fields } — patch fields. Same clamp rule on toolConfig.
188
+ Hard rules: cannot target self; cannot target ancestors.
189
+ delete-agent { agentId } — cascade-delete: schedules, memories, team memberships,
190
+ then the agent itself.
191
+
192
+ ACTIONS — teams (per-agent multi-select scope: member / ownedByMe / all; default all-false = disabled):
193
+ list-teams — list teams reachable in your scope
194
+ create-team { name, description?, color? } — create. createdBy is set to YOU automatically.
195
+ update-team { teamId, name?, description?, color? }
196
+ delete-team { teamId } — removes the team; member assignments die with it.
197
+ add-team-member { teamId, agentId } — add an agent to a team. agentId='self' adds caller.
198
+ remove-team-member { teamId, agentId } — remove an agent. agentId='self' = leave the team.
199
+
200
+ ACTIONS — flows (per-agent permission: 'disabled' | 'self-created' | 'all'; default 'disabled'):
201
+ list-flows — list all flows reachable in your scope (read is unrestricted at any non-disabled level)
202
+ get-flow { flowId } — fetch a flow's full definition (nodes + edges + variables)
203
+ create-flow { name, description?, nodes, edges?, variables? }
204
+ — create a new flow. Definition must pass schema validation.
205
+ createdBy is set to YOU automatically.
206
+ Per-creator quota (maxFlowsCreated, default unlimited) applies.
207
+ update-flow { flowId, ...fields } — patch a flow you have permission to mutate.
208
+ The full re-validated definition is required when changing nodes/edges.
209
+ delete-flow { flowId } — cascade-delete: run history + checkpoints + version snapshots.
210
+ execute-flow { flowId, input? } — kick off a run. Returns runId for polling.
211
+ dry-run-flow { flowId } OR { flow } — lint-only check without execution; surfaces structural issues
212
+ (orphaned nodes, missing required fields, etc.) before live runs.
213
+
214
+ list-capabilities — what permission level + scope this agent has
215
+
216
+ EXAMPLES:
217
+
218
+ # Wake me up tomorrow at 9am with a check-in prompt
219
+ {
220
+ "toolId": "platformcontrol",
221
+ "action": "schedule-self-resume",
222
+ "runAt": "2026-04-27T09:00:00Z",
223
+ "prompt": "Resume task: review yesterday's PR comments and respond.",
224
+ "name": "Morning PR review"
225
+ }
226
+
227
+ # Recurring agent-self schedule
228
+ {
229
+ "toolId": "platformcontrol",
230
+ "action": "create-schedule",
231
+ "name": "Hourly health check",
232
+ "prompt": "Run smoke tests and report status.",
233
+ "cronExpression": "0 * * * *",
234
+ "targetAgentId": "self"
235
+ }
236
+
237
+ # List my own schedules
238
+ { "toolId": "platformcontrol", "action": "list-schedules" }
239
+
240
+ NOTES:
241
+ - Flow schedules are NEVER reachable from this tool, regardless of permission level.
242
+ - 'self' is a sugar for the calling agent's id; explicit ids work too.
243
+ - cronExpression accepts the standard 5-field cron OR a preset name (call list-presets).
244
+ `;
245
+ }
246
+
247
+ getSupportedActions() { return [...SUPPORTED_ACTIONS]; }
248
+
249
+ async execute(params, context = {}) {
250
+ if (!params || typeof params !== 'object') {
251
+ return { success: false, error: 'params must be an object' };
252
+ }
253
+ // Reverse-forgiveness: accept plural {actions:[{...}]} envelope.
254
+ if (Array.isArray(params?.actions) && params.actions.length > 0) {
255
+ const a = params.actions[0] || {};
256
+ params = { ...params, ...a, action: a.type || a.action || params.action };
257
+ }
258
+ const action = params.action || 'list-capabilities';
259
+ const callerAgentId = context?.agentId || null;
260
+
261
+ const cfg = this.getEffectiveConfig(context, {});
262
+ const level = getScheduleAccessLevel(cfg);
263
+
264
+ // list-capabilities is always allowed — agent needs to discover its
265
+ // own permissions even when scheduledTasks is 'disabled'.
266
+ if (action === 'list-capabilities') {
267
+ return this._listCapabilities(level, cfg);
268
+ }
269
+
270
+ // Per-feature gating: each action's permission lives in its own slice
271
+ // of toolConfig.platformcontrol. Schedule actions get scheduleLevel
272
+ // gating; agent / team actions handle their own gates inside their
273
+ // handlers (which need the full cfg anyway for the privilege clamp).
274
+ const isScheduleAction = action.includes('schedule') || action === 'list-presets';
275
+ if (isScheduleAction) {
276
+ if (level === SCHEDULE_ACCESS_LEVELS.DISABLED) {
277
+ return {
278
+ success: false,
279
+ disabled: true,
280
+ error: 'Scheduled-tasks access is disabled for this agent. Enable it in the platformcontrol tool configurator.',
281
+ };
282
+ }
283
+ if (!this.scheduleService) {
284
+ return { success: false, error: 'ScheduleService is not available on this server.' };
285
+ }
286
+ }
287
+
288
+ // IMPORTANT: `await` inside the try so async-handler rejections are
289
+ // caught here. Without await, a `return this._createSchedule(...)`
290
+ // hands an un-awaited Promise back; rejections propagate past this
291
+ // try/catch and surface as raw exceptions to the caller.
292
+ try {
293
+ switch (action) {
294
+ case 'list-schedules': return await this._listSchedules(level, callerAgentId);
295
+ case 'get-schedule': return await this._getSchedule(level, callerAgentId, params);
296
+ case 'create-schedule': return await this._createSchedule(level, callerAgentId, params);
297
+ case 'update-schedule': return await this._updateSchedule(level, callerAgentId, params);
298
+ case 'delete-schedule': return await this._deleteSchedule(level, callerAgentId, params);
299
+ case 'toggle-schedule': return await this._toggleSchedule(level, callerAgentId, params);
300
+ case 'trigger-schedule': return await this._triggerSchedule(level, callerAgentId, params);
301
+ case 'list-presets': return await this._listPresets();
302
+ case 'schedule-self-resume': return await this._scheduleSelfResume(level, callerAgentId, params);
303
+ // Agent CRUD
304
+ case 'list-agents': return await this._listAgents(cfg, callerAgentId);
305
+ case 'create-agent': return await this._createAgent(cfg, callerAgentId, params);
306
+ case 'update-agent': return await this._updateAgent(cfg, callerAgentId, params);
307
+ case 'delete-agent': return await this._deleteAgent(cfg, callerAgentId, params);
308
+ // Team CRUD + membership
309
+ case 'list-teams': return await this._listTeams(cfg, callerAgentId);
310
+ case 'create-team': return await this._createTeam(cfg, callerAgentId, params);
311
+ case 'update-team': return await this._updateTeam(cfg, callerAgentId, params);
312
+ case 'delete-team': return await this._deleteTeam(cfg, callerAgentId, params);
313
+ case 'add-team-member': return await this._addTeamMember(cfg, callerAgentId, params);
314
+ case 'remove-team-member': return await this._removeTeamMember(cfg, callerAgentId, params);
315
+ // Flow CRUD + execution
316
+ case 'list-flows': return await this._listFlows(cfg, callerAgentId);
317
+ case 'get-flow': return await this._getFlow(cfg, callerAgentId, params);
318
+ case 'create-flow': return await this._createFlow(cfg, callerAgentId, params);
319
+ case 'update-flow': return await this._updateFlow(cfg, callerAgentId, params);
320
+ case 'delete-flow': return await this._deleteFlow(cfg, callerAgentId, params);
321
+ case 'execute-flow': return await this._executeFlow(cfg, callerAgentId, params);
322
+ case 'dry-run-flow': return await this._dryRunFlow(cfg, callerAgentId, params);
323
+ default:
324
+ return {
325
+ success: false,
326
+ error: `Unknown action: ${action}. Supported: ${SUPPORTED_ACTIONS.join(', ')}`,
327
+ };
328
+ }
329
+ } catch (err) {
330
+ this.logger?.error?.('[platformcontrol] action failed', { action, error: err?.message });
331
+ return { success: false, error: err?.message || 'unknown error' };
332
+ }
333
+ }
334
+
335
+ _listCapabilities(level, fullCfg) {
336
+ const agentLevel = getAgentAccessLevel(fullCfg);
337
+ const teamScope = getTeamScope(fullCfg);
338
+ const teamsDisabled = isTeamAccessDisabled(fullCfg);
339
+ return {
340
+ success: true,
341
+ action: 'list-capabilities',
342
+ capabilities: {
343
+ scheduledTasks: {
344
+ level,
345
+ canListOwn: level !== SCHEDULE_ACCESS_LEVELS.DISABLED,
346
+ canListAll: level === SCHEDULE_ACCESS_LEVELS.ALL,
347
+ canMutateOwn: level !== SCHEDULE_ACCESS_LEVELS.DISABLED,
348
+ canMutateAll: level === SCHEDULE_ACCESS_LEVELS.ALL,
349
+ notes: [
350
+ 'Flow-target schedules are not reachable from this tool.',
351
+ 'Default level is "disabled". Configure via the platformcontrol tool configurator.',
352
+ ],
353
+ },
354
+ agents: {
355
+ level: agentLevel,
356
+ maxAgentsCreated: getMaxAgentsCreated(fullCfg),
357
+ canList: agentLevel !== AGENT_ACCESS_LEVELS.DISABLED,
358
+ canCreate: agentLevel !== AGENT_ACCESS_LEVELS.DISABLED,
359
+ canMutateSelfCreated: agentLevel !== AGENT_ACCESS_LEVELS.DISABLED,
360
+ canMutateAll: agentLevel === AGENT_ACCESS_LEVELS.ALL,
361
+ notes: [
362
+ 'Hard rule: an agent cannot modify itself or any of its ancestors.',
363
+ 'When configuring another agent\'s toolConfig, child permissions are clamped to your level.',
364
+ 'maxAgentsCreated null = unlimited; counted only against agents YOU created via this tool.',
365
+ ],
366
+ },
367
+ teams: {
368
+ scope: teamScope,
369
+ disabled: teamsDisabled,
370
+ notes: [
371
+ 'Multi-select scope: you can act on teams you are a member of, teams you created, or all (each independently).',
372
+ 'Leaving a team you\'re a member of is allowed.',
373
+ ],
374
+ },
375
+ flows: {
376
+ level: getFlowAccessLevel(fullCfg),
377
+ maxFlowsCreated: getMaxFlowsCreated(fullCfg),
378
+ canList: getFlowAccessLevel(fullCfg) !== FLOW_ACCESS_LEVELS.DISABLED,
379
+ canCreate: getFlowAccessLevel(fullCfg) !== FLOW_ACCESS_LEVELS.DISABLED,
380
+ canMutateSelfCreated: getFlowAccessLevel(fullCfg) !== FLOW_ACCESS_LEVELS.DISABLED,
381
+ canMutateAll: getFlowAccessLevel(fullCfg) === FLOW_ACCESS_LEVELS.ALL,
382
+ notes: [
383
+ 'Permission key: flows. Default level is "disabled".',
384
+ 'Flow definitions are validated against the v2 schema before save — invalid flows are rejected with detailed errors.',
385
+ 'delete-flow cascades through run history, checkpoints, and version snapshots.',
386
+ 'execute-flow respects existing flow permission levels — at "self-created" level you can only run flows you created.',
387
+ ],
388
+ },
389
+ },
390
+ };
391
+ }
392
+
393
+ _listSchedules(level, callerAgentId) {
394
+ const all = this.scheduleService.listSchedules();
395
+ const accessible = filterAccessibleSchedules(level, callerAgentId, all);
396
+ return {
397
+ success: true,
398
+ action: 'list-schedules',
399
+ count: accessible.length,
400
+ schedules: accessible,
401
+ scope: level,
402
+ };
403
+ }
404
+
405
+ _getSchedule(level, callerAgentId, params) {
406
+ const id = params?.scheduleId;
407
+ if (!id) return { success: false, error: 'scheduleId is required' };
408
+ const schedule = this.scheduleService.getSchedule(id);
409
+ if (!schedule) return { success: false, error: `Schedule not found: ${id}` };
410
+ const access = checkScheduleAccess(level, callerAgentId, schedule);
411
+ if (!access.allow) {
412
+ // Surface as not-found rather than scope-denied so 'own' agents
413
+ // cannot probe for the existence of other agents' schedules by id.
414
+ return { success: false, error: `Schedule not found: ${id}` };
415
+ }
416
+ return { success: true, action: 'get-schedule', schedule };
417
+ }
418
+
419
+ /**
420
+ * Resolve `targetAgentId` from the create/self-resume params:
421
+ * - 'self' → callerAgentId
422
+ * - undefined → callerAgentId (default to self)
423
+ * - any string → the explicit id
424
+ *
425
+ * In 'own' mode we then assert the resolved id MUST equal callerAgentId.
426
+ */
427
+ _resolveTargetAgentId(level, callerAgentId, raw) {
428
+ if (raw === 'self' || raw === undefined || raw === null || raw === '') return callerAgentId;
429
+ if (typeof raw !== 'string') {
430
+ throw new Error('targetAgentId must be a string (or "self")');
431
+ }
432
+ if (level === SCHEDULE_ACCESS_LEVELS.OWN && raw !== callerAgentId) {
433
+ throw new Error(
434
+ `Out of scope: 'own' mode requires targetAgentId === this agent's id ("${callerAgentId}"). ` +
435
+ `Set the platformcontrol tool to 'all' for cross-agent scheduling.`
436
+ );
437
+ }
438
+ return raw;
439
+ }
440
+
441
+ async _createSchedule(level, callerAgentId, params) {
442
+ const targetAgentId = this._resolveTargetAgentId(level, callerAgentId, params?.targetAgentId);
443
+ const built = {
444
+ name: params?.name,
445
+ prompt: params?.prompt,
446
+ targetType: 'agent', // tool only ever creates agent-target schedules
447
+ targetId: targetAgentId,
448
+ cronExpression: params?.cronExpression,
449
+ enabled: params?.enabled !== false, // default true
450
+ description: params?.description || '',
451
+ startDate: params?.startDate || null,
452
+ endDate: params?.endDate || null,
453
+ maxRuns: params?.maxRuns ?? null,
454
+ runOnce: !!params?.runOnce,
455
+ };
456
+ const schedule = await this.scheduleService.createSchedule(built);
457
+ return { success: true, action: 'create-schedule', schedule };
458
+ }
459
+
460
+ async _updateSchedule(level, callerAgentId, params) {
461
+ const id = params?.scheduleId;
462
+ if (!id) return { success: false, error: 'scheduleId is required' };
463
+ const existing = this.scheduleService.getSchedule(id);
464
+ if (!existing) return { success: false, error: `Schedule not found: ${id}` };
465
+ const access = checkScheduleAccess(level, callerAgentId, existing);
466
+ if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
467
+
468
+ // 'own' mode cannot reassign a schedule to a different target.
469
+ if (params?.targetAgentId !== undefined && params.targetAgentId !== null) {
470
+ const desired = this._resolveTargetAgentId(level, callerAgentId, params.targetAgentId);
471
+ if (level === SCHEDULE_ACCESS_LEVELS.OWN && desired !== callerAgentId) {
472
+ return {
473
+ success: false,
474
+ error: `Out of scope: 'own' mode cannot reassign schedule "${id}" to another agent.`,
475
+ };
476
+ }
477
+ }
478
+
479
+ const updates = {};
480
+ const PASS = ['name', 'description', 'prompt', 'cronExpression', 'enabled', 'startDate', 'endDate', 'maxRuns', 'runOnce'];
481
+ for (const k of PASS) if (params[k] !== undefined) updates[k] = params[k];
482
+ if (params?.targetAgentId !== undefined && params.targetAgentId !== null) {
483
+ updates.targetId = this._resolveTargetAgentId(level, callerAgentId, params.targetAgentId);
484
+ }
485
+ const schedule = await this.scheduleService.updateSchedule(id, updates);
486
+ return { success: true, action: 'update-schedule', schedule };
487
+ }
488
+
489
+ async _deleteSchedule(level, callerAgentId, params) {
490
+ const id = params?.scheduleId;
491
+ if (!id) return { success: false, error: 'scheduleId is required' };
492
+ const existing = this.scheduleService.getSchedule(id);
493
+ if (!existing) return { success: false, error: `Schedule not found: ${id}` };
494
+ const access = checkScheduleAccess(level, callerAgentId, existing);
495
+ if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
496
+ await this.scheduleService.deleteSchedule(id);
497
+ return { success: true, action: 'delete-schedule', scheduleId: id };
498
+ }
499
+
500
+ async _toggleSchedule(level, callerAgentId, params) {
501
+ const id = params?.scheduleId;
502
+ if (!id) return { success: false, error: 'scheduleId is required' };
503
+ const existing = this.scheduleService.getSchedule(id);
504
+ if (!existing) return { success: false, error: `Schedule not found: ${id}` };
505
+ const access = checkScheduleAccess(level, callerAgentId, existing);
506
+ if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
507
+
508
+ const next = typeof params?.enabled === 'boolean' ? params.enabled : !existing.enabled;
509
+ const schedule = await this.scheduleService.updateSchedule(id, { enabled: next });
510
+ return { success: true, action: 'toggle-schedule', scheduleId: id, enabled: schedule.enabled };
511
+ }
512
+
513
+ async _triggerSchedule(level, callerAgentId, params) {
514
+ const id = params?.scheduleId;
515
+ if (!id) return { success: false, error: 'scheduleId is required' };
516
+ const existing = this.scheduleService.getSchedule(id);
517
+ if (!existing) return { success: false, error: `Schedule not found: ${id}` };
518
+ const access = checkScheduleAccess(level, callerAgentId, existing);
519
+ if (!access.allow) return { success: false, error: `Schedule not found: ${id}` };
520
+
521
+ // ScheduleService doesn't expose a public "trigger now" API yet; the
522
+ // closest is _executeSchedule which is internal. Use the private path
523
+ // when available; otherwise mark this action as not-implemented so the
524
+ // agent gets clear feedback rather than a silent stub.
525
+ if (typeof this.scheduleService._executeSchedule === 'function') {
526
+ // Fire-and-forget — execution may take a while, the agent shouldn't block.
527
+ this.scheduleService._executeSchedule(existing).catch(err => {
528
+ this.logger?.warn?.('[platformcontrol] trigger-schedule execution failed', {
529
+ scheduleId: id, error: err?.message,
530
+ });
531
+ });
532
+ return { success: true, action: 'trigger-schedule', scheduleId: id, triggered: true };
533
+ }
534
+ return {
535
+ success: false,
536
+ error: 'trigger-schedule is not supported on this version of ScheduleService.',
537
+ };
538
+ }
539
+
540
+ _listPresets() {
541
+ // Presets live in scheduleService; expose via a stable surface.
542
+ // CRON_PRESETS isn't exported, so we surface the names plus a note
543
+ // pointing to scheduleService.listSchedules's cronPreset field.
544
+ // (TODO: export presets from scheduleService and read them here.)
545
+ return {
546
+ success: true,
547
+ action: 'list-presets',
548
+ presets: [
549
+ 'every-minute', 'every-5-minutes', 'every-15-minutes', 'every-30-minutes',
550
+ 'every-hour', 'every-6-hours', 'every-12-hours',
551
+ 'daily', 'daily-morning', 'daily-evening',
552
+ 'weekdays', 'weekends', 'weekly-monday', 'monthly',
553
+ ],
554
+ note: 'Pass a preset name as cronExpression OR a raw 5-field cron string.',
555
+ };
556
+ }
557
+
558
+ async _scheduleSelfResume(level, callerAgentId, params) {
559
+ // TODO: rethink ergonomics. Currently a thin wrapper around
560
+ // create-schedule with runOnce + cron derived from a future ISO
561
+ // datetime. May become a first-class wake-at primitive after we
562
+ // see how agents actually plan around it.
563
+ if (!callerAgentId) {
564
+ return { success: false, error: 'callerAgentId is required (no context.agentId)' };
565
+ }
566
+ const { runAt, prompt, name } = params || {};
567
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
568
+ return { success: false, error: 'prompt (non-empty string) is required' };
569
+ }
570
+ let cron, normalizedIso;
571
+ try {
572
+ ({ cron, normalizedIso } = isoToOneShotCron(runAt));
573
+ } catch (err) {
574
+ return { success: false, error: err.message };
575
+ }
576
+ const schedule = await this.scheduleService.createSchedule({
577
+ name: name || `Self-resume @ ${normalizedIso}`,
578
+ description: 'One-shot self-resume created via platformcontrol.schedule-self-resume',
579
+ prompt,
580
+ targetType: 'agent',
581
+ targetId: callerAgentId,
582
+ cronExpression: cron,
583
+ runOnce: true,
584
+ enabled: true,
585
+ });
586
+ return {
587
+ success: true,
588
+ action: 'schedule-self-resume',
589
+ runAt: normalizedIso,
590
+ cronExpression: cron,
591
+ schedule,
592
+ };
593
+ }
594
+
595
+ // ─── Agent + team helpers ─────────────────────────────────────────
596
+
597
+ /**
598
+ * Snapshot all agents into a Map keyed by id, used to build the
599
+ * `getAgent` lookup the ancestry walker needs. Done once per action
600
+ * so a single call doesn't hit the agent pool repeatedly.
601
+ *
602
+ * Returns a tuple { agents, getAgent, isProtectedFromCaller }.
603
+ * isProtectedFromCaller is curried with the lookup so it can be
604
+ * passed into checkAgentMutationAccess as opts.isProtectedFromCaller.
605
+ */
606
+ async _agentSnapshot() {
607
+ if (!this.agentPool || typeof this.agentPool.getAllAgents !== 'function') {
608
+ return { agents: [], getAgent: () => null, isProtectedFromCaller: () => false };
609
+ }
610
+ const all = await this.agentPool.getAllAgents();
611
+ // Normalize to array (getAllAgents may return a Map).
612
+ const arr = (all instanceof Map) ? Array.from(all.values()) : Array.isArray(all) ? all : [];
613
+ const getAgent = makeAgentLookup(arr);
614
+ const protect = (callerId, targetId) => isProtectedFromCaller(callerId, targetId, getAgent);
615
+ return { agents: arr, getAgent, isProtectedFromCaller: protect };
616
+ }
617
+
618
+ /** Project an agent into the safe summary shape we expose to other agents. */
619
+ _projectAgent(a) {
620
+ if (!a) return null;
621
+ return {
622
+ id: a.id,
623
+ name: a.name,
624
+ mode: a.mode,
625
+ status: a.status,
626
+ currentModel: a.currentModel,
627
+ preferredModel: a.preferredModel || null,
628
+ capabilities: Array.isArray(a.capabilities) ? a.capabilities.slice() : [],
629
+ skills: Array.isArray(a.skills) ? a.skills.slice() : [],
630
+ createdBy: a.createdBy || null,
631
+ createdAt: a.createdAt || null,
632
+ };
633
+ }
634
+
635
+ // ─── Agent CRUD ───────────────────────────────────────────────────
636
+
637
+ async _listAgents(cfg, callerAgentId) {
638
+ const level = getAgentAccessLevel(cfg);
639
+ if (level === AGENT_ACCESS_LEVELS.DISABLED) {
640
+ return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
641
+ }
642
+ if (!this.agentPool) return { success: false, error: 'AgentPool unavailable.' };
643
+ const { agents } = await this._agentSnapshot(callerAgentId);
644
+ // Read is unrestricted at any non-disabled level — agents need to
645
+ // enumerate to make decisions. Mutations are gated separately.
646
+ const list = agents.map(a => this._projectAgent(a));
647
+ return { success: true, action: 'list-agents', count: list.length, agents: list, scope: level };
648
+ }
649
+
650
+ async _createAgent(cfg, callerAgentId, params) {
651
+ const level = getAgentAccessLevel(cfg);
652
+ if (level === AGENT_ACCESS_LEVELS.DISABLED) {
653
+ return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
654
+ }
655
+ if (!this.agentPool || typeof this.agentPool.createAgent !== 'function') {
656
+ return { success: false, error: 'AgentPool unavailable.' };
657
+ }
658
+ const { name, systemPrompt, model } = params || {};
659
+ if (!name || typeof name !== 'string' || !name.trim()) {
660
+ return { success: false, error: 'name (non-empty string) is required' };
661
+ }
662
+ if (!systemPrompt || typeof systemPrompt !== 'string') {
663
+ return { success: false, error: 'systemPrompt (string) is required' };
664
+ }
665
+
666
+ // Per-creator quota — count alive agents whose createdBy === caller.
667
+ const max = getMaxAgentsCreated(cfg);
668
+ if (max !== null) {
669
+ const { agents } = await this._agentSnapshot(callerAgentId);
670
+ const owned = agents.filter(a => a.createdBy === callerAgentId).length;
671
+ if (owned >= max) {
672
+ return {
673
+ success: false,
674
+ error: `Per-creator agent quota exhausted (${owned}/${max}). Delete one of your created agents or ask the user to raise maxAgentsCreated.`,
675
+ };
676
+ }
677
+ }
678
+
679
+ // Privilege clamp on the requested toolConfig.platformcontrol.
680
+ let pcSliceClamps = [];
681
+ let nextToolConfig = (params.toolConfig && typeof params.toolConfig === 'object') ? { ...params.toolConfig } : {};
682
+ if (nextToolConfig.platformcontrol && typeof nextToolConfig.platformcontrol === 'object') {
683
+ const callerPc = (cfg && typeof cfg === 'object') ? cfg : {};
684
+ const { config: clampedPc, clamps } = clampToolConfigForChild(callerPc, nextToolConfig.platformcontrol);
685
+ nextToolConfig.platformcontrol = clampedPc;
686
+ pcSliceClamps = clamps;
687
+ }
688
+
689
+ const newConfig = {
690
+ name: name.trim(),
691
+ description: typeof params.description === 'string' ? params.description : '',
692
+ systemPrompt,
693
+ model: model || undefined,
694
+ preferredModel: params.preferredModel || model || undefined,
695
+ capabilities: Array.isArray(params.capabilities) ? params.capabilities.slice() : [],
696
+ skills: Array.isArray(params.skills) ? params.skills.slice() : [],
697
+ toolConfig: nextToolConfig,
698
+ directoryAccess: params.directoryAccess || undefined,
699
+ // KEY: tag the new agent's parent so ancestry rules apply forever after.
700
+ createdBy: callerAgentId,
701
+ };
702
+
703
+ const created = await this.agentPool.createAgent(newConfig);
704
+ return {
705
+ success: true,
706
+ action: 'create-agent',
707
+ agent: this._projectAgent(created),
708
+ clamps: pcSliceClamps,
709
+ };
710
+ }
711
+
712
+ async _updateAgent(cfg, callerAgentId, params) {
713
+ const level = getAgentAccessLevel(cfg);
714
+ if (level === AGENT_ACCESS_LEVELS.DISABLED) {
715
+ return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
716
+ }
717
+ if (!this.agentPool) return { success: false, error: 'AgentPool unavailable.' };
718
+ const targetId = params?.agentId;
719
+ if (!targetId) return { success: false, error: 'agentId is required' };
720
+
721
+ const { agents, isProtectedFromCaller: protect } = await this._agentSnapshot(callerAgentId);
722
+ const target = agents.find(a => a.id === targetId);
723
+ if (!target) return { success: false, error: `Agent not found: ${targetId}` };
724
+
725
+ const access = checkAgentMutationAccess(level, callerAgentId, target, {
726
+ isProtectedFromCaller: protect,
727
+ });
728
+ if (!access.allow) {
729
+ return { success: false, error: access.reason };
730
+ }
731
+
732
+ // Build the updates patch — everything the user UI accepts, with the
733
+ // privilege clamp applied to toolConfig.platformcontrol.
734
+ const updates = {};
735
+ const PASS_THROUGH = ['name', 'description', 'systemPrompt', 'model', 'preferredModel',
736
+ 'capabilities', 'skills', 'directoryAccess'];
737
+ for (const k of PASS_THROUGH) if (params[k] !== undefined) updates[k] = params[k];
738
+
739
+ let pcSliceClamps = [];
740
+ if (params.toolConfig && typeof params.toolConfig === 'object') {
741
+ const next = { ...params.toolConfig };
742
+ if (next.platformcontrol && typeof next.platformcontrol === 'object') {
743
+ const callerPc = (cfg && typeof cfg === 'object') ? cfg : {};
744
+ const { config: clampedPc, clamps } = clampToolConfigForChild(callerPc, next.platformcontrol);
745
+ next.platformcontrol = clampedPc;
746
+ pcSliceClamps = clamps;
747
+ }
748
+ updates.toolConfig = next;
749
+ }
750
+
751
+ const updated = await this.agentPool.updateAgent(targetId, updates);
752
+ return {
753
+ success: true,
754
+ action: 'update-agent',
755
+ agent: this._projectAgent(updated || target),
756
+ clamps: pcSliceClamps,
757
+ };
758
+ }
759
+
760
+ async _deleteAgent(cfg, callerAgentId, params) {
761
+ const level = getAgentAccessLevel(cfg);
762
+ if (level === AGENT_ACCESS_LEVELS.DISABLED) {
763
+ return { success: false, disabled: true, error: 'Agent management is disabled for this caller.' };
764
+ }
765
+ if (!this.agentPool) return { success: false, error: 'AgentPool unavailable.' };
766
+ const targetId = params?.agentId;
767
+ if (!targetId) return { success: false, error: 'agentId is required' };
768
+
769
+ const { agents, isProtectedFromCaller: protect } = await this._agentSnapshot(callerAgentId);
770
+ const target = agents.find(a => a.id === targetId);
771
+ if (!target) return { success: false, error: `Agent not found: ${targetId}` };
772
+
773
+ const access = checkAgentMutationAccess(level, callerAgentId, target, {
774
+ isProtectedFromCaller: protect,
775
+ });
776
+ if (!access.allow) return { success: false, error: access.reason };
777
+
778
+ const report = await cascadeDeleteAgent({
779
+ agentId: targetId,
780
+ scheduleService: this.scheduleService,
781
+ memoryService: this.memoryService,
782
+ stateManager: this.stateManager,
783
+ agentPool: this.agentPool,
784
+ logger: this.logger,
785
+ });
786
+ return {
787
+ success: report.agentDeleted,
788
+ action: 'delete-agent',
789
+ agentId: targetId,
790
+ report,
791
+ };
792
+ }
793
+
794
+ // ─── Team CRUD + membership ───────────────────────────────────────
795
+
796
+ async _listTeams(cfg, callerAgentId) {
797
+ if (isTeamAccessDisabled(cfg)) {
798
+ return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
799
+ }
800
+ if (!this.stateManager || typeof this.stateManager.getAllTeams !== 'function') {
801
+ return { success: false, error: 'StateManager unavailable.' };
802
+ }
803
+ const all = await this.stateManager.getAllTeams();
804
+ const scope = getTeamScope(cfg);
805
+ // Per the design: read is unrestricted within scope (not unlimited
806
+ // like agents). The agent only sees teams it's reachable for —
807
+ // listing teams it can never act on adds noise without value.
808
+ const accessible = filterAccessibleTeams(scope, callerAgentId, all);
809
+ return { success: true, action: 'list-teams', count: accessible.length, teams: accessible, scope };
810
+ }
811
+
812
+ async _createTeam(cfg, callerAgentId, params) {
813
+ if (isTeamAccessDisabled(cfg)) {
814
+ return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
815
+ }
816
+ if (!this.stateManager || typeof this.stateManager.createTeam !== 'function') {
817
+ return { success: false, error: 'StateManager unavailable.' };
818
+ }
819
+ const { name, description, color } = params || {};
820
+ if (!name || typeof name !== 'string' || !name.trim()) {
821
+ return { success: false, error: 'name (non-empty string) is required' };
822
+ }
823
+ const team = await this.stateManager.createTeam({
824
+ name: name.trim(),
825
+ description: description || '',
826
+ color,
827
+ createdBy: callerAgentId,
828
+ });
829
+ return { success: true, action: 'create-team', team };
830
+ }
831
+
832
+ /**
833
+ * Verify scope access on an EXISTING team. Returns the team or an
834
+ * error result — the caller propagates either.
835
+ */
836
+ async _resolveTeamForAccess(cfg, callerAgentId, teamId) {
837
+ if (!teamId) return { error: 'teamId is required' };
838
+ const team = await this.stateManager.getTeam(teamId);
839
+ if (!team) return { error: `Team not found: ${teamId}` };
840
+ const scope = getTeamScope(cfg);
841
+ const access = checkTeamAccess(scope, callerAgentId, team);
842
+ if (!access.allow) return { error: `Team not found: ${teamId}` }; // hide existence
843
+ return { team };
844
+ }
845
+
846
+ async _updateTeam(cfg, callerAgentId, params) {
847
+ if (isTeamAccessDisabled(cfg)) {
848
+ return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
849
+ }
850
+ const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
851
+ if (got.error) return { success: false, error: got.error };
852
+ const updates = {};
853
+ for (const k of ['name', 'description', 'color']) {
854
+ if (params[k] !== undefined) updates[k] = params[k];
855
+ }
856
+ const updated = await this.stateManager.updateTeam(params.teamId, updates);
857
+ return { success: true, action: 'update-team', team: updated };
858
+ }
859
+
860
+ async _deleteTeam(cfg, callerAgentId, params) {
861
+ if (isTeamAccessDisabled(cfg)) {
862
+ return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
863
+ }
864
+ const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
865
+ if (got.error) return { success: false, error: got.error };
866
+ const report = await cascadeDeleteTeam({
867
+ teamId: params.teamId,
868
+ stateManager: this.stateManager,
869
+ logger: this.logger,
870
+ });
871
+ return { success: report.teamDeleted, action: 'delete-team', teamId: params.teamId, report };
872
+ }
873
+
874
+ /**
875
+ * Adding a member is a team mutation. Self-add is allowed iff the
876
+ * caller's scope already covers the team (e.g. 'all', 'ownedByMe').
877
+ * The hard rules apply to AGENT mutations — adding agentX to a team
878
+ * is a TEAM mutation, not an agent mutation, so no ancestor check.
879
+ */
880
+ async _addTeamMember(cfg, callerAgentId, params) {
881
+ if (isTeamAccessDisabled(cfg)) {
882
+ return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
883
+ }
884
+ const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
885
+ if (got.error) return { success: false, error: got.error };
886
+ const agentId = params?.agentId === 'self' ? callerAgentId : params?.agentId;
887
+ if (!agentId) return { success: false, error: 'agentId is required' };
888
+ const team = await this.stateManager.addAgentToTeam(params.teamId, agentId);
889
+ return { success: true, action: 'add-team-member', teamId: params.teamId, agentId, team };
890
+ }
891
+
892
+ /**
893
+ * Removing a member is a team mutation. SPECIAL CASE: if the agent
894
+ * is removing ITSELF (leaving), allow it as long as the team is
895
+ * within scope — leaving a team is not self-modification of the
896
+ * agent record, it's modification of the team's member list.
897
+ * If the agent has 'member' scope and is leaving, the team IS in
898
+ * scope by definition, so this just works.
899
+ */
900
+ async _removeTeamMember(cfg, callerAgentId, params) {
901
+ if (isTeamAccessDisabled(cfg)) {
902
+ return { success: false, disabled: true, error: 'Team management is disabled for this caller.' };
903
+ }
904
+ const got = await this._resolveTeamForAccess(cfg, callerAgentId, params?.teamId);
905
+ if (got.error) return { success: false, error: got.error };
906
+ const agentId = params?.agentId === 'self' ? callerAgentId : params?.agentId;
907
+ if (!agentId) return { success: false, error: 'agentId is required' };
908
+ const team = await this.stateManager.removeAgentFromTeam(params.teamId, agentId);
909
+ return { success: true, action: 'remove-team-member', teamId: params.teamId, agentId, team };
910
+ }
911
+
912
+ // ─── Flow CRUD + execution ─────────────────────────────────────────
913
+ //
914
+ // Permission key: `flows`. Default 'disabled'. Same shape as agents:
915
+ // 'self-created' = only flows you authored, 'all' = admin scope.
916
+ // Read endpoints (list/get) are unrestricted at any non-disabled
917
+ // level so an agent can browse what's available before deciding which
918
+ // to run; mutations (create/update/delete/execute/dry-run) all go
919
+ // through `checkFlowMutationAccess`.
920
+
921
+ /** Resolve the project dir the same way every other path here does. */
922
+ _flowProjectDir() {
923
+ return this.stateManager?.config?.project?.directory
924
+ || this.stateManager?.config?.projectDir
925
+ || process.cwd();
926
+ }
927
+
928
+ async _listFlows(cfg, callerAgentId) {
929
+ const level = getFlowAccessLevel(cfg);
930
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
931
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
932
+ }
933
+ if (!this.stateManager) {
934
+ return { success: false, error: 'StateManager not available.' };
935
+ }
936
+ const all = await this.stateManager.getAllFlows(this._flowProjectDir());
937
+ // List is unrestricted at any non-disabled level — agents can see
938
+ // what they could run / what the platform offers. Mutability is
939
+ // surfaced per-row so the LLM knows what it can act on.
940
+ const list = (all || []).map(f => ({
941
+ id: f.id,
942
+ name: f.name,
943
+ description: f.description || '',
944
+ version: f.version,
945
+ createdBy: f.createdBy || null,
946
+ nodeCount: Array.isArray(f.nodes) ? f.nodes.length : 0,
947
+ mutable: checkFlowMutationAccess(level, callerAgentId, f).allow,
948
+ }));
949
+ return { success: true, action: 'list-flows', count: list.length, flows: list, scope: level };
950
+ }
951
+
952
+ async _getFlow(cfg, callerAgentId, params) {
953
+ const level = getFlowAccessLevel(cfg);
954
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
955
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
956
+ }
957
+ if (!params?.flowId) return { success: false, error: 'flowId is required' };
958
+ const flow = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
959
+ if (!flow) return { success: false, error: `Flow not found: ${params.flowId}` };
960
+ return {
961
+ success: true, action: 'get-flow', flow,
962
+ mutable: checkFlowMutationAccess(level, callerAgentId, flow).allow,
963
+ };
964
+ }
965
+
966
+ async _createFlow(cfg, callerAgentId, params) {
967
+ const level = getFlowAccessLevel(cfg);
968
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
969
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
970
+ }
971
+ if (!params?.name || typeof params.name !== 'string') {
972
+ return { success: false, error: 'name is required' };
973
+ }
974
+ if (!Array.isArray(params.nodes) || params.nodes.length === 0) {
975
+ return { success: false, error: 'nodes array is required (must have at least one node)' };
976
+ }
977
+ // Quota enforcement — count flows the caller has authored so far.
978
+ const max = getMaxFlowsCreated(cfg);
979
+ if (max !== null) {
980
+ const owned = (await this.stateManager.getAllFlows(this._flowProjectDir()))
981
+ .filter(f => f.createdBy === callerAgentId);
982
+ if (owned.length >= max) {
983
+ return {
984
+ success: false,
985
+ error: `maxFlowsCreated quota reached: you have created ${owned.length} of ${max} allowed flows.`,
986
+ };
987
+ }
988
+ }
989
+ // Schema validation runs inside stateManager.createFlow already
990
+ // (via the route layer); we additionally call validate here so an
991
+ // invalid create returns a clean structured error rather than going
992
+ // through the route. Defensive — duplicate guard, no harm.
993
+ const created = await this.stateManager.createFlow({
994
+ name: params.name,
995
+ description: params.description || '',
996
+ nodes: params.nodes,
997
+ edges: params.edges || [],
998
+ variables: params.variables || {},
999
+ // Stamp the caller as the author so self-created scope can find it.
1000
+ createdBy: callerAgentId,
1001
+ }, this._flowProjectDir());
1002
+ return { success: true, action: 'create-flow', flow: created };
1003
+ }
1004
+
1005
+ async _updateFlow(cfg, callerAgentId, params) {
1006
+ const level = getFlowAccessLevel(cfg);
1007
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1008
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1009
+ }
1010
+ if (!params?.flowId) return { success: false, error: 'flowId is required' };
1011
+ const target = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1012
+ if (!target) return { success: false, error: `Flow not found: ${params.flowId}` };
1013
+ const access = checkFlowMutationAccess(level, callerAgentId, target);
1014
+ if (!access.allow) return { success: false, error: access.reason };
1015
+ // Strip identity-level fields the caller shouldn't be able to spoof.
1016
+ const { id: _id, createdBy: _cb, version: _v, ...patch } = params;
1017
+ const updated = await this.stateManager.updateFlow(params.flowId, patch, this._flowProjectDir());
1018
+ return { success: true, action: 'update-flow', flow: updated };
1019
+ }
1020
+
1021
+ async _deleteFlow(cfg, callerAgentId, params) {
1022
+ const level = getFlowAccessLevel(cfg);
1023
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1024
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1025
+ }
1026
+ if (!params?.flowId) return { success: false, error: 'flowId is required' };
1027
+ const target = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1028
+ if (!target) return { success: false, error: `Flow not found: ${params.flowId}` };
1029
+ const access = checkFlowMutationAccess(level, callerAgentId, target);
1030
+ if (!access.allow) return { success: false, error: access.reason };
1031
+ await this.stateManager.deleteFlow(params.flowId, this._flowProjectDir());
1032
+ return { success: true, action: 'delete-flow', flowId: params.flowId };
1033
+ }
1034
+
1035
+ async _executeFlow(cfg, callerAgentId, params) {
1036
+ const level = getFlowAccessLevel(cfg);
1037
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1038
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1039
+ }
1040
+ if (!params?.flowId) return { success: false, error: 'flowId is required' };
1041
+ if (!this.flowExecutor) return { success: false, error: 'FlowExecutor not available on this server.' };
1042
+ const target = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1043
+ if (!target) return { success: false, error: `Flow not found: ${params.flowId}` };
1044
+ // Execute counts as a mutation (it consumes credits + writes run history).
1045
+ const access = checkFlowMutationAccess(level, callerAgentId, target);
1046
+ if (!access.allow) return { success: false, error: access.reason };
1047
+ const run = await this.flowExecutor.executeFlow(params.flowId, params.input || {}, {
1048
+ projectDir: this._flowProjectDir(),
1049
+ // Tag the run with the agent that triggered it so audit trails
1050
+ // attribute correctly when an agent kicks a flow off via the tool.
1051
+ triggeredBy: { kind: 'agent', agentId: callerAgentId },
1052
+ });
1053
+ return { success: true, action: 'execute-flow', runId: run?.runId || run?.id || null, status: run?.status || 'queued' };
1054
+ }
1055
+
1056
+ async _dryRunFlow(cfg, callerAgentId, params) {
1057
+ const level = getFlowAccessLevel(cfg);
1058
+ if (level === FLOW_ACCESS_LEVELS.DISABLED) {
1059
+ return { success: false, disabled: true, error: 'Flow management is disabled for this caller.' };
1060
+ }
1061
+ // Dry-run accepts EITHER an existing flowId OR an inline flow def
1062
+ // (so an agent can lint a draft before saving).
1063
+ let flow;
1064
+ if (params?.flowId) {
1065
+ flow = await this.stateManager.getFlow(params.flowId, this._flowProjectDir());
1066
+ if (!flow) return { success: false, error: `Flow not found: ${params.flowId}` };
1067
+ } else if (params?.flow && typeof params.flow === 'object') {
1068
+ flow = params.flow;
1069
+ } else {
1070
+ return { success: false, error: 'Pass either { flowId } or { flow: {...} }' };
1071
+ }
1072
+ if (!this.flowExecutor || typeof this.flowExecutor.dryRun !== 'function') {
1073
+ return { success: false, error: 'FlowExecutor.dryRun not available on this server.' };
1074
+ }
1075
+ const report = await this.flowExecutor.dryRun(flow);
1076
+ return { success: true, action: 'dry-run-flow', report };
1077
+ }
1078
+ }
1079
+
1080
+ export default PlatformControlTool;
1081
+ export { PlatformControlTool };