machinaos 0.0.78 → 0.0.80

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 (743) hide show
  1. package/.env.template +74 -5
  2. package/{workflows/AI Assistant_workflow-1778504793388-ou1m1tz2x.json → .machina/workflows/AI Assistant_example_workflow-1779017037684-e2e5da7a.json } +164 -105
  3. package/{workflows/AI Employee_example_workflow-1777720598005-u4cm858dv.json → .machina/workflows/AI Employee_example_workflow-1779102911870-cbc76c82.json } +582 -328
  4. package/{workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json → .machina/workflows/Claude Assistant_example_workflow-1779095939967-2369cff4.json } +152 -83
  5. package/README.md +5 -2
  6. package/bin/cli.js +2 -2
  7. package/{machina → cli}/__main__.py +11 -7
  8. package/cli/_common.py +122 -0
  9. package/cli/buildenv.py +40 -0
  10. package/cli/cli.py +204 -0
  11. package/{machina → cli}/colors.py +10 -2
  12. package/cli/commands/__init__.py +1 -0
  13. package/cli/commands/_temporal_specs.py +59 -0
  14. package/{machina → cli}/commands/build.py +35 -45
  15. package/cli/commands/clean.py +141 -0
  16. package/cli/commands/daemon/__init__.py +47 -0
  17. package/cli/commands/daemon/_state.py +97 -0
  18. package/cli/commands/daemon/restart.py +14 -0
  19. package/cli/commands/daemon/start.py +49 -0
  20. package/cli/commands/daemon/status.py +20 -0
  21. package/cli/commands/daemon/stop.py +22 -0
  22. package/{machina → cli}/commands/dev.py +32 -42
  23. package/{machina → cli}/commands/docs.py +13 -11
  24. package/{machina → cli}/commands/start.py +69 -62
  25. package/{machina → cli}/commands/stop.py +7 -10
  26. package/{machina → cli}/commands/version.py +12 -6
  27. package/cli/config.py +170 -0
  28. package/cli/platform_.py +169 -0
  29. package/{machina → cli}/ports.py +42 -3
  30. package/{machina → cli}/run.py +29 -2
  31. package/{machina → cli}/supervisor.py +29 -12
  32. package/{machina → cli}/tcp.py +6 -2
  33. package/{machina → cli}/tree.py +38 -11
  34. package/client/dist/assets/{ActionBar-Du2MSFSz.js → ActionBar-Cjr3TF7g.js} +1 -1
  35. package/client/dist/assets/{ApiKeyInput-k2LBmBjb.js → ApiKeyInput-DIJE2PVA.js} +1 -1
  36. package/client/dist/assets/{ApiKeyPanel-C_bV9U0X.js → ApiKeyPanel-CPmye7uh.js} +1 -1
  37. package/client/dist/assets/{ApiUsageSection-CmVfwZzL.js → ApiUsageSection-TF_7gH2D.js} +1 -1
  38. package/client/dist/assets/{EmailPanel-CeKIMGu-.js → EmailPanel-Bs-xvbKR.js} +1 -1
  39. package/client/dist/assets/{OAuthPanel-KA3t3Q2K.js → OAuthPanel-BDtVJhAV.js} +1 -1
  40. package/client/dist/assets/{QrPairingPanel-NgNpJNuk.js → QrPairingPanel-BwJehTuZ.js} +1 -1
  41. package/client/dist/assets/{RateLimitSection-Du5YNVIA.js → RateLimitSection-CfNOoPIS.js} +1 -1
  42. package/client/dist/assets/{StatusCard-DNLyayXc.js → StatusCard-DkwIrgdP.js} +1 -1
  43. package/client/dist/assets/index-P2FzntoL.js +165 -0
  44. package/client/dist/index.html +1 -1
  45. package/client/package.json +1 -1
  46. package/client/src/Dashboard.tsx +128 -76
  47. package/client/src/adapters/nodeSpecToDescription.ts +7 -0
  48. package/client/src/assets/icons/index.test.ts +10 -0
  49. package/client/src/assets/icons/index.ts +16 -3
  50. package/client/src/components/AIAgentNode.tsx +8 -8
  51. package/client/src/components/ParameterRenderer.tsx +6 -3
  52. package/client/src/components/SkillEditorModal.tsx +1 -0
  53. package/client/src/components/credentials/panels/EmailPanel.tsx +2 -0
  54. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +2 -0
  55. package/client/src/components/credentials/sections/RateLimitSection.tsx +1 -0
  56. package/client/src/components/icons/AIProviderIcons.tsx +1 -0
  57. package/client/src/components/maps/GoogleMapsPicker.tsx +1 -0
  58. package/client/src/components/parameterPanel/InputSection.tsx +1 -0
  59. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +1 -0
  60. package/client/src/components/parameterPanel/OutputSection.tsx +1 -0
  61. package/client/src/components/ui/ComponentPalette.tsx +1 -0
  62. package/client/src/components/ui/MapSelector.tsx +1 -0
  63. package/client/src/components/ui/NodeContextMenu.tsx +3 -3
  64. package/client/src/components/ui/SettingsPanel.tsx +1 -0
  65. package/client/src/components/ui/action-button.tsx +1 -0
  66. package/client/src/components/ui/badge.tsx +1 -0
  67. package/client/src/components/ui/button.tsx +1 -0
  68. package/client/src/components/ui/form.tsx +1 -0
  69. package/client/src/components/ui/tabs.tsx +1 -0
  70. package/client/src/contexts/AuthContext.tsx +1 -0
  71. package/client/src/contexts/ThemeContext.tsx +1 -0
  72. package/client/src/contexts/WebSocketContext.tsx +104 -34
  73. package/client/src/hooks/__tests__/useApiKeys.test.ts +2 -2
  74. package/client/src/hooks/useReactFlowNodes.ts +1 -0
  75. package/client/src/hooks/useWorkflowValidation.ts +142 -0
  76. package/client/src/lib/nodeSpec.ts +1 -0
  77. package/client/src/test/providers.tsx +1 -0
  78. package/client/src/types/__tests__/cloudEvents.test.ts +5 -2
  79. package/client/src/types/cloudEvents.ts +19 -7
  80. package/client/src/utils/nodeUtils.ts +1 -1
  81. package/client/src/utils/workflow.ts +8 -2
  82. package/client/src/utils/workflowExport.ts +60 -3
  83. package/package.json +24 -23
  84. package/scripts/install.js +16 -27
  85. package/scripts/migrate_icons.py +3 -1
  86. package/scripts/migrate_skill_icons.py +6 -7
  87. package/scripts/postinstall.js +11 -9
  88. package/server/config/ai_cli_providers.json +2 -3
  89. package/server/config/credential_providers.json +15 -15
  90. package/server/config/llm_defaults.json +1 -1
  91. package/server/config/model_registry.json +416 -611
  92. package/server/constants.py +285 -223
  93. package/server/core/__init__.py +1 -1
  94. package/server/core/cache.py +9 -29
  95. package/server/core/cleanup.py +12 -24
  96. package/server/core/config.py +148 -24
  97. package/server/core/container.py +68 -59
  98. package/server/core/credential_backends.py +5 -13
  99. package/server/core/credentials_database.py +13 -43
  100. package/server/core/database.py +292 -353
  101. package/server/core/health.py +4 -5
  102. package/server/core/logging.py +241 -87
  103. package/server/core/paths.py +285 -0
  104. package/server/core/tracing.py +2 -8
  105. package/server/gunicorn.conf.py +1 -0
  106. package/server/main.py +150 -74
  107. package/server/middleware/auth.py +18 -24
  108. package/server/models/__init__.py +1 -1
  109. package/server/models/auth.py +5 -12
  110. package/server/models/database.py +36 -68
  111. package/server/models/node_metadata.py +25 -18
  112. package/server/nodejs/dist/index.js +107 -0
  113. package/server/nodes/README.md +11 -5
  114. package/server/nodes/__init__.py +1 -1
  115. package/server/nodes/_visuals.py +146 -14
  116. package/server/nodes/agent/_events.py +124 -0
  117. package/server/nodes/agent/_handles.py +15 -29
  118. package/server/nodes/agent/_inline.py +28 -25
  119. package/server/nodes/agent/_specialized.py +30 -15
  120. package/server/nodes/agent/{ai_agent.py → ai_agent/__init__.py} +33 -17
  121. package/server/nodes/agent/ai_agent/meta.json +3 -0
  122. package/server/nodes/agent/{ai_employee.py → ai_employee/__init__.py} +5 -2
  123. package/server/nodes/agent/ai_employee/meta.json +3 -0
  124. package/server/nodes/agent/{android_agent.py → android_agent/__init__.py} +1 -1
  125. package/server/nodes/agent/android_agent/meta.json +3 -0
  126. package/server/nodes/agent/{autonomous_agent.py → autonomous_agent/__init__.py} +2 -1
  127. package/server/nodes/agent/autonomous_agent/meta.json +3 -0
  128. package/server/nodes/agent/{chat_agent.py → chat_agent/__init__.py} +29 -12
  129. package/server/nodes/agent/chat_agent/meta.json +3 -0
  130. package/server/nodes/agent/{claude_code_agent.py → claude_code_agent/__init__.py} +192 -95
  131. package/server/nodes/agent/claude_code_agent/_handlers.py +169 -0
  132. package/server/{services/claude_oauth.py → nodes/agent/claude_code_agent/_oauth.py} +26 -13
  133. package/server/nodes/agent/claude_code_agent/_pool.py +1020 -0
  134. package/server/nodes/agent/claude_code_agent/_provider.py +513 -0
  135. package/server/nodes/agent/claude_code_agent/_skills.py +245 -0
  136. package/server/nodes/agent/claude_code_agent/meta.json +3 -0
  137. package/server/nodes/agent/{codex_agent.py → codex_agent/__init__.py} +26 -35
  138. package/server/nodes/agent/codex_agent/meta.json +3 -0
  139. package/server/nodes/agent/{coding_agent.py → coding_agent/__init__.py} +1 -1
  140. package/server/nodes/agent/coding_agent/meta.json +3 -0
  141. package/server/nodes/agent/{consumer_agent.py → consumer_agent/__init__.py} +1 -1
  142. package/server/nodes/agent/consumer_agent/meta.json +3 -0
  143. package/server/nodes/agent/{orchestrator_agent.py → orchestrator_agent/__init__.py} +5 -2
  144. package/server/nodes/agent/orchestrator_agent/meta.json +3 -0
  145. package/server/nodes/agent/{payments_agent.py → payments_agent/__init__.py} +1 -1
  146. package/server/nodes/agent/payments_agent/meta.json +3 -0
  147. package/server/nodes/agent/{productivity_agent.py → productivity_agent/__init__.py} +1 -1
  148. package/server/nodes/agent/productivity_agent/meta.json +3 -0
  149. package/server/nodes/agent/{rlm_agent.py → rlm_agent/__init__.py} +18 -17
  150. package/server/nodes/agent/rlm_agent/meta.json +3 -0
  151. package/server/nodes/agent/{social_agent.py → social_agent/__init__.py} +1 -1
  152. package/server/nodes/agent/social_agent/meta.json +3 -0
  153. package/server/nodes/agent/{task_agent.py → task_agent/__init__.py} +1 -1
  154. package/server/nodes/agent/task_agent/meta.json +3 -0
  155. package/server/nodes/agent/{tool_agent.py → tool_agent/__init__.py} +1 -1
  156. package/server/nodes/agent/tool_agent/meta.json +3 -0
  157. package/server/nodes/agent/{travel_agent.py → travel_agent/__init__.py} +1 -1
  158. package/server/nodes/agent/travel_agent/meta.json +3 -0
  159. package/server/nodes/agent/{web_agent.py → web_agent/__init__.py} +1 -1
  160. package/server/nodes/agent/web_agent/meta.json +3 -0
  161. package/server/nodes/android/__init__.py +24 -0
  162. package/server/nodes/android/_base.py +93 -76
  163. package/server/nodes/android/_dispatcher.py +140 -223
  164. package/server/nodes/android/_events.py +154 -0
  165. package/server/nodes/android/_handlers.py +13 -7
  166. package/server/nodes/android/_option_loaders.py +1 -4
  167. package/server/nodes/android/_refresh.py +27 -37
  168. package/server/nodes/android/_relay/broadcaster.py +25 -41
  169. package/server/nodes/android/_relay/client.py +23 -42
  170. package/server/nodes/android/_relay/manager.py +1 -0
  171. package/server/nodes/android/_relay/protocol.py +6 -0
  172. package/server/nodes/android/_router.py +48 -133
  173. package/server/nodes/android/{airplane_mode_control.py → airplane_mode_control/__init__.py} +2 -1
  174. package/server/nodes/android/airplane_mode_control/meta.json +3 -0
  175. package/server/nodes/android/{app_launcher.py → app_launcher/__init__.py} +2 -1
  176. package/server/nodes/android/app_launcher/meta.json +3 -0
  177. package/server/nodes/android/{app_list.py → app_list/__init__.py} +2 -1
  178. package/server/nodes/android/app_list/meta.json +3 -0
  179. package/server/nodes/android/{audio_automation.py → audio_automation/__init__.py} +2 -1
  180. package/server/nodes/android/audio_automation/meta.json +3 -0
  181. package/server/nodes/android/{battery_monitor.py → battery_monitor/__init__.py} +2 -1
  182. package/server/nodes/android/battery_monitor/meta.json +3 -0
  183. package/server/nodes/android/{bluetooth_automation.py → bluetooth_automation/__init__.py} +2 -1
  184. package/server/nodes/android/bluetooth_automation/meta.json +3 -0
  185. package/server/nodes/android/{camera_control.py → camera_control/__init__.py} +2 -1
  186. package/server/nodes/android/camera_control/meta.json +3 -0
  187. package/server/nodes/android/{device_state_automation.py → device_state_automation/__init__.py} +2 -1
  188. package/server/nodes/android/device_state_automation/meta.json +3 -0
  189. package/server/nodes/android/{environmental_sensors.py → environmental_sensors/__init__.py} +2 -1
  190. package/server/nodes/android/environmental_sensors/meta.json +3 -0
  191. package/server/nodes/android/{location.py → location/__init__.py} +2 -1
  192. package/server/nodes/android/location/meta.json +3 -0
  193. package/server/nodes/android/{media_control.py → media_control/__init__.py} +2 -1
  194. package/server/nodes/android/media_control/meta.json +3 -0
  195. package/server/nodes/android/{motion_detection.py → motion_detection/__init__.py} +2 -1
  196. package/server/nodes/android/motion_detection/meta.json +3 -0
  197. package/server/nodes/android/{network_monitor.py → network_monitor/__init__.py} +2 -1
  198. package/server/nodes/android/network_monitor/meta.json +3 -0
  199. package/server/nodes/android/{screen_control_automation.py → screen_control_automation/__init__.py} +2 -1
  200. package/server/nodes/android/screen_control_automation/meta.json +3 -0
  201. package/server/nodes/android/{system_info.py → system_info/__init__.py} +2 -1
  202. package/server/nodes/android/system_info/meta.json +3 -0
  203. package/server/nodes/android/{wifi_automation.py → wifi_automation/__init__.py} +2 -1
  204. package/server/nodes/android/wifi_automation/meta.json +3 -0
  205. package/server/nodes/browser/__init__.py +22 -1
  206. package/server/nodes/browser/_install.py +63 -0
  207. package/server/nodes/browser/_service.py +21 -25
  208. package/server/nodes/browser/{browser.py → browser/__init__.py} +58 -25
  209. package/server/nodes/browser/browser/meta.json +3 -0
  210. package/server/nodes/chat/{chat_history.py → chat_history/__init__.py} +2 -4
  211. package/server/nodes/chat/chat_history/meta.json +3 -0
  212. package/server/nodes/chat/{chat_send.py → chat_send/__init__.py} +2 -4
  213. package/server/nodes/chat/chat_send/icon.svg +1 -0
  214. package/server/nodes/chat/chat_send/meta.json +3 -0
  215. package/server/nodes/code/_base.py +1 -1
  216. package/server/nodes/code/{javascript_executor.py → javascript_executor/__init__.py} +5 -5
  217. package/server/nodes/code/javascript_executor/meta.json +3 -0
  218. package/server/nodes/code/{python_executor.py → python_executor/__init__.py} +32 -14
  219. package/server/nodes/code/python_executor/meta.json +3 -0
  220. package/server/nodes/code/{typescript_executor.py → typescript_executor/__init__.py} +5 -5
  221. package/server/nodes/code/typescript_executor/meta.json +3 -0
  222. package/server/nodes/document/{document_parser.py → document_parser/__init__.py} +26 -15
  223. package/server/nodes/document/document_parser/meta.json +3 -0
  224. package/server/nodes/document/{embedding_generator.py → embedding_generator/__init__.py} +16 -9
  225. package/server/nodes/document/embedding_generator/meta.json +3 -0
  226. package/server/nodes/document/{file_downloader.py → file_downloader/__init__.py} +30 -20
  227. package/server/nodes/document/file_downloader/meta.json +3 -0
  228. package/server/nodes/document/{http_scraper.py → http_scraper/__init__.py} +31 -21
  229. package/server/nodes/document/http_scraper/meta.json +3 -0
  230. package/server/nodes/document/{text_chunker.py → text_chunker/__init__.py} +17 -12
  231. package/server/nodes/document/text_chunker/meta.json +3 -0
  232. package/server/nodes/document/{vector_store.py → vector_store/__init__.py} +88 -72
  233. package/server/nodes/document/vector_store/meta.json +3 -0
  234. package/server/nodes/email/__init__.py +9 -2
  235. package/server/nodes/email/_events.py +54 -0
  236. package/server/nodes/email/_filters.py +3 -3
  237. package/server/nodes/email/_himalaya.py +95 -50
  238. package/server/nodes/email/_service.py +23 -13
  239. package/server/nodes/email/{email_read.py → email_read/__init__.py} +23 -11
  240. package/server/nodes/email/email_read/icon.svg +6 -0
  241. package/server/nodes/email/email_read/meta.json +3 -0
  242. package/server/nodes/email/{email_receive.py → email_receive/__init__.py} +45 -23
  243. package/server/nodes/email/email_receive/meta.json +3 -0
  244. package/server/nodes/email/{email_send.py → email_send/__init__.py} +13 -7
  245. package/server/nodes/email/email_send/meta.json +3 -0
  246. package/server/nodes/filesystem/_backend.py +1 -5
  247. package/server/nodes/filesystem/{file_modify.py → file_modify/__init__.py} +10 -5
  248. package/server/nodes/filesystem/file_modify/meta.json +3 -0
  249. package/server/nodes/filesystem/{file_read.py → file_read/__init__.py} +7 -3
  250. package/server/nodes/filesystem/file_read/meta.json +3 -0
  251. package/server/nodes/filesystem/{fs_search.py → fs_search/__init__.py} +11 -3
  252. package/server/nodes/filesystem/fs_search/meta.json +3 -0
  253. package/server/nodes/filesystem/{shell.py → shell/__init__.py} +12 -5
  254. package/server/nodes/filesystem/shell/meta.json +3 -0
  255. package/server/nodes/google/__init__.py +12 -0
  256. package/server/nodes/google/_auth_helper.py +7 -13
  257. package/server/nodes/google/_base.py +14 -11
  258. package/server/nodes/google/_credentials.py +2 -1
  259. package/server/nodes/google/_events.py +47 -0
  260. package/server/nodes/google/_filters.py +3 -3
  261. package/server/nodes/google/_gmail.py +70 -47
  262. package/server/nodes/google/_handlers.py +3 -1
  263. package/server/nodes/google/_oauth.py +25 -11
  264. package/server/nodes/google/_option_loaders.py +9 -30
  265. package/server/nodes/google/_refresh.py +8 -12
  266. package/server/nodes/google/_router.py +4 -5
  267. package/server/nodes/google/{calendar.py → calendar/__init__.py} +87 -64
  268. package/server/nodes/google/calendar/meta.json +3 -0
  269. package/server/nodes/google/{contacts.py → contacts/__init__.py} +84 -72
  270. package/server/nodes/google/contacts/meta.json +3 -0
  271. package/server/nodes/google/{drive.py → drive/__init__.py} +87 -72
  272. package/server/nodes/google/drive/meta.json +3 -0
  273. package/server/nodes/google/{gmail.py → gmail/__init__.py} +73 -39
  274. package/server/nodes/google/gmail/meta.json +3 -0
  275. package/server/nodes/google/{gmail_receive.py → gmail_receive/__init__.py} +31 -24
  276. package/server/nodes/google/gmail_receive/icon.svg +7 -0
  277. package/server/nodes/google/gmail_receive/meta.json +3 -0
  278. package/server/nodes/google/google.svg +7 -0
  279. package/server/nodes/google/{sheets.py → sheets/__init__.py} +54 -42
  280. package/server/nodes/google/sheets/meta.json +3 -0
  281. package/server/nodes/google/{tasks.py → tasks/__init__.py} +56 -43
  282. package/server/nodes/google/tasks/meta.json +3 -0
  283. package/server/nodes/groups.py +28 -28
  284. package/server/nodes/location/__init__.py +31 -1
  285. package/server/nodes/location/_credentials.py +1 -6
  286. package/server/nodes/location/_service.py +88 -107
  287. package/server/nodes/location/{gmaps_create.py → gmaps_create/__init__.py} +6 -6
  288. package/server/nodes/location/gmaps_create/meta.json +3 -0
  289. package/server/nodes/location/{gmaps_locations.py → gmaps_locations/__init__.py} +8 -6
  290. package/server/nodes/location/gmaps_locations/meta.json +3 -0
  291. package/server/nodes/location/{gmaps_nearby_places.py → gmaps_nearby_places/__init__.py} +8 -6
  292. package/server/nodes/location/gmaps_nearby_places/meta.json +3 -0
  293. package/server/nodes/model/_base.py +10 -7
  294. package/server/nodes/model/_credentials.py +10 -10
  295. package/server/nodes/model/_local_validator.py +28 -24
  296. package/server/nodes/model/{anthropic_chat_model.py → anthropic_chat_model/__init__.py} +5 -3
  297. package/server/nodes/model/anthropic_chat_model/meta.json +3 -0
  298. package/server/nodes/model/{cerebras_chat_model.py → cerebras_chat_model/__init__.py} +5 -3
  299. package/server/nodes/model/cerebras_chat_model/meta.json +3 -0
  300. package/server/nodes/model/{deepseek_chat_model.py → deepseek_chat_model/__init__.py} +8 -4
  301. package/server/nodes/model/deepseek_chat_model/meta.json +3 -0
  302. package/server/nodes/model/{gemini_chat_model.py → gemini_chat_model/__init__.py} +5 -3
  303. package/server/nodes/model/gemini_chat_model/meta.json +3 -0
  304. package/server/nodes/model/{groq_chat_model.py → groq_chat_model/__init__.py} +2 -2
  305. package/server/nodes/model/groq_chat_model/meta.json +3 -0
  306. package/server/nodes/model/{kimi_chat_model.py → kimi_chat_model/__init__.py} +2 -2
  307. package/server/nodes/model/kimi_chat_model/meta.json +3 -0
  308. package/server/nodes/model/{lmstudio_chat_model.py → lmstudio_chat_model/__init__.py} +2 -2
  309. package/server/nodes/model/lmstudio_chat_model/meta.json +3 -0
  310. package/server/nodes/model/{mistral_chat_model.py → mistral_chat_model/__init__.py} +2 -2
  311. package/server/nodes/model/mistral_chat_model/meta.json +3 -0
  312. package/server/nodes/model/{ollama_chat_model.py → ollama_chat_model/__init__.py} +2 -2
  313. package/server/nodes/model/ollama_chat_model/meta.json +3 -0
  314. package/server/nodes/model/{openai_chat_model.py → openai_chat_model/__init__.py} +8 -4
  315. package/server/nodes/model/openai_chat_model/meta.json +3 -0
  316. package/server/nodes/model/{openrouter_chat_model.py → openrouter_chat_model/__init__.py} +8 -4
  317. package/server/nodes/model/openrouter_chat_model/meta.json +3 -0
  318. package/server/nodes/proxy/_usage.py +14 -15
  319. package/server/nodes/proxy/{proxy_config.py → proxy_config/__init__.py} +39 -30
  320. package/server/nodes/proxy/proxy_config/meta.json +3 -0
  321. package/server/nodes/proxy/{proxy_request.py → proxy_request/__init__.py} +30 -16
  322. package/server/nodes/proxy/proxy_request/meta.json +3 -0
  323. package/server/nodes/proxy/{proxy_status.py → proxy_status/__init__.py} +2 -0
  324. package/server/nodes/proxy/proxy_status/meta.json +3 -0
  325. package/server/nodes/scheduler/{cron_scheduler.py → cron_scheduler/__init__.py} +96 -23
  326. package/server/nodes/scheduler/cron_scheduler/_workflow.py +155 -0
  327. package/server/nodes/scheduler/cron_scheduler/meta.json +3 -0
  328. package/server/nodes/scheduler/{timer.py → timer/__init__.py} +6 -5
  329. package/server/nodes/scheduler/timer/meta.json +3 -0
  330. package/server/nodes/scraper/_credentials.py +0 -1
  331. package/server/nodes/scraper/{apify_actor.py → apify_actor/__init__.py} +44 -35
  332. package/server/nodes/scraper/apify_actor/icon.svg +5 -0
  333. package/server/nodes/scraper/apify_actor/meta.json +3 -0
  334. package/server/nodes/scraper/{crawlee_scraper.py → crawlee_scraper/__init__.py} +96 -57
  335. package/server/nodes/scraper/crawlee_scraper/meta.json +3 -0
  336. package/server/nodes/search/{brave_search.py → brave_search/__init__.py} +6 -5
  337. package/server/nodes/search/brave_search/icon.svg +3 -0
  338. package/server/nodes/search/brave_search/meta.json +3 -0
  339. package/server/nodes/search/{duckduckgo_search.py → duckduckgo_search/__init__.py} +17 -6
  340. package/server/nodes/search/duckduckgo_search/meta.json +3 -0
  341. package/server/nodes/search/{perplexity_search.py → perplexity_search/__init__.py} +4 -5
  342. package/server/nodes/search/perplexity_search/icon.svg +3 -0
  343. package/server/nodes/search/perplexity_search/meta.json +3 -0
  344. package/server/nodes/search/{serper_search.py → serper_search/__init__.py} +32 -25
  345. package/server/nodes/search/serper_search/icon.svg +3 -0
  346. package/server/nodes/search/serper_search/meta.json +3 -0
  347. package/server/nodes/skill/__init__.py +21 -1
  348. package/server/nodes/skill/_expander.py +75 -0
  349. package/server/nodes/skill/{master_skill.py → master_skill/__init__.py} +2 -8
  350. package/server/nodes/skill/master_skill/_events.py +84 -0
  351. package/server/nodes/skill/master_skill/meta.json +3 -0
  352. package/server/nodes/skill/{simple_memory.py → simple_memory/__init__.py} +8 -16
  353. package/server/nodes/skill/simple_memory/meta.json +3 -0
  354. package/server/nodes/social/_base.py +223 -231
  355. package/server/nodes/social/{social_receive.py → social_receive/__init__.py} +38 -13
  356. package/server/nodes/social/social_receive/meta.json +3 -0
  357. package/server/nodes/social/{social_send.py → social_send/__init__.py} +71 -29
  358. package/server/nodes/social/social_send/icon.svg +1 -0
  359. package/server/nodes/social/social_send/meta.json +3 -0
  360. package/server/nodes/stripe/__init__.py +7 -3
  361. package/server/nodes/stripe/_credentials.py +0 -1
  362. package/server/nodes/stripe/_handlers.py +18 -7
  363. package/server/nodes/stripe/_install.py +14 -15
  364. package/server/nodes/stripe/_source.py +5 -5
  365. package/server/nodes/stripe/icon.svg +1 -0
  366. package/server/nodes/stripe/meta.json +3 -0
  367. package/server/nodes/stripe/stripe_action.py +4 -4
  368. package/server/nodes/stripe/stripe_receive.py +6 -9
  369. package/server/nodes/telegram/__init__.py +13 -0
  370. package/server/nodes/telegram/_credentials.py +2 -7
  371. package/server/nodes/telegram/_events.py +167 -0
  372. package/server/nodes/telegram/_filters.py +3 -11
  373. package/server/nodes/telegram/_handlers.py +17 -7
  374. package/server/nodes/telegram/_refresh.py +24 -34
  375. package/server/nodes/telegram/_service.py +29 -45
  376. package/server/nodes/telegram/meta.json +3 -0
  377. package/server/nodes/telegram/telegram.svg +3 -0
  378. package/server/nodes/telegram/telegram_receive.py +38 -18
  379. package/server/nodes/telegram/telegram_send.py +21 -19
  380. package/server/nodes/text/{file_handler.py → file_handler/__init__.py} +7 -1
  381. package/server/nodes/text/file_handler/meta.json +3 -0
  382. package/server/nodes/text/{text_generator.py → text_generator/__init__.py} +2 -1
  383. package/server/nodes/text/text_generator/meta.json +3 -0
  384. package/server/nodes/tool/{agent_builder.py → agent_builder/__init__.py} +105 -100
  385. package/server/nodes/tool/agent_builder/_events.py +91 -0
  386. package/server/nodes/tool/agent_builder/meta.json +3 -0
  387. package/server/nodes/tool/{calculator_tool.py → calculator_tool/__init__.py} +19 -7
  388. package/server/nodes/tool/calculator_tool/meta.json +3 -0
  389. package/server/nodes/tool/{current_time_tool.py → current_time_tool/__init__.py} +6 -4
  390. package/server/nodes/tool/current_time_tool/meta.json +3 -0
  391. package/server/nodes/tool/{task_manager.py → task_manager/__init__.py} +17 -18
  392. package/server/nodes/tool/task_manager/meta.json +3 -0
  393. package/server/nodes/tool/{write_todos.py → write_todos/__init__.py} +20 -6
  394. package/server/nodes/tool/write_todos/meta.json +3 -0
  395. package/server/nodes/trigger/{chat_trigger.py → chat_trigger/__init__.py} +11 -7
  396. package/server/nodes/trigger/chat_trigger/_events.py +53 -0
  397. package/server/nodes/trigger/chat_trigger/meta.json +3 -0
  398. package/server/nodes/trigger/{task_trigger.py → task_trigger/__init__.py} +10 -7
  399. package/server/nodes/trigger/task_trigger/meta.json +3 -0
  400. package/server/nodes/trigger/{webhook_trigger.py → webhook_trigger/__init__.py} +10 -7
  401. package/server/nodes/trigger/webhook_trigger/_events.py +54 -0
  402. package/server/nodes/trigger/webhook_trigger/meta.json +3 -0
  403. package/server/nodes/twitter/__init__.py +7 -1
  404. package/server/nodes/twitter/_base.py +86 -61
  405. package/server/nodes/twitter/_credentials.py +7 -5
  406. package/server/nodes/twitter/_events.py +101 -0
  407. package/server/nodes/twitter/_filters.py +9 -9
  408. package/server/nodes/twitter/_handlers.py +3 -1
  409. package/server/nodes/twitter/_oauth.py +1 -2
  410. package/server/nodes/twitter/_refresh.py +8 -12
  411. package/server/nodes/twitter/{twitter_receive.py → twitter_receive/__init__.py} +7 -7
  412. package/server/nodes/twitter/twitter_receive/icon.svg +1 -0
  413. package/server/nodes/twitter/twitter_receive/meta.json +3 -0
  414. package/server/nodes/twitter/{twitter_search.py → twitter_search/__init__.py} +16 -11
  415. package/server/nodes/twitter/twitter_search/icon.svg +1 -0
  416. package/server/nodes/twitter/twitter_search/meta.json +3 -0
  417. package/server/nodes/twitter/{twitter_send.py → twitter_send/__init__.py} +60 -27
  418. package/server/nodes/twitter/twitter_send/icon.svg +1 -0
  419. package/server/nodes/twitter/twitter_send/meta.json +3 -0
  420. package/server/nodes/twitter/{twitter_user.py → twitter_user/__init__.py} +34 -19
  421. package/server/nodes/twitter/twitter_user/icon.svg +1 -0
  422. package/server/nodes/twitter/twitter_user/meta.json +3 -0
  423. package/server/nodes/utility/{console.py → console/__init__.py} +17 -22
  424. package/server/nodes/utility/console/meta.json +3 -0
  425. package/server/nodes/utility/{http_request.py → http_request/__init__.py} +9 -6
  426. package/server/nodes/utility/http_request/meta.json +3 -0
  427. package/server/nodes/utility/{process_manager.py → process_manager/__init__.py} +10 -6
  428. package/server/nodes/utility/process_manager/meta.json +3 -0
  429. package/server/nodes/utility/team_monitor/meta.json +3 -0
  430. package/server/nodes/utility/{webhook_response.py → webhook_response/__init__.py} +12 -7
  431. package/server/nodes/utility/webhook_response/meta.json +3 -0
  432. package/server/nodes/visuals.json +69 -251
  433. package/server/nodes/whatsapp/__init__.py +24 -0
  434. package/server/nodes/whatsapp/_base.py +283 -338
  435. package/server/nodes/whatsapp/_credentials.py +44 -0
  436. package/server/nodes/whatsapp/_events.py +277 -0
  437. package/server/nodes/whatsapp/_filters.py +36 -37
  438. package/server/nodes/whatsapp/_handlers.py +2 -0
  439. package/server/nodes/whatsapp/_option_loaders.py +1 -3
  440. package/server/nodes/whatsapp/_refresh.py +13 -18
  441. package/server/nodes/whatsapp/_runtime.py +9 -6
  442. package/server/nodes/whatsapp/_service.py +89 -152
  443. package/server/nodes/whatsapp/meta.json +3 -0
  444. package/server/nodes/whatsapp/whatsapp_db.py +116 -54
  445. package/server/nodes/whatsapp/whatsapp_receive.py +30 -13
  446. package/server/nodes/whatsapp/whatsapp_send.py +60 -37
  447. package/server/nodes/workflow/{start.py → start/__init__.py} +1 -4
  448. package/server/nodes/workflow/start/meta.json +3 -0
  449. package/server/package-lock.json +3 -3
  450. package/server/package.json +3 -0
  451. package/server/pyproject.toml +39 -10
  452. package/server/requirements.txt +3 -5
  453. package/server/routers/__init__.py +1 -1
  454. package/server/routers/auth.py +16 -56
  455. package/server/routers/database.py +27 -50
  456. package/server/routers/nodejs_compat.py +25 -87
  457. package/server/routers/schemas.py +66 -2
  458. package/server/routers/webhook.py +12 -12
  459. package/server/routers/websocket.py +312 -1716
  460. package/server/routers/workflow.py +28 -53
  461. package/server/scripts/smoke_test_skills.py +178 -0
  462. package/server/services/__init__.py +1 -1
  463. package/server/services/_supervisor/process.py +9 -3
  464. package/server/services/_supervisor/registry.py +3 -3
  465. package/server/services/_supervisor/util.py +1 -1
  466. package/server/services/agent_team.py +15 -43
  467. package/server/services/agent_teams/__init__.py +17 -0
  468. package/server/services/agent_teams/handlers.py +195 -0
  469. package/server/services/ai.py +853 -1108
  470. package/server/services/auth.py +10 -34
  471. package/server/services/chat_client.py +5 -34
  472. package/server/services/circuit_breaker.py +2 -6
  473. package/server/services/cli_agent/__init__.py +28 -4
  474. package/server/services/cli_agent/_cli_auth.py +61 -0
  475. package/server/services/cli_agent/_handlers.py +24 -183
  476. package/server/services/cli_agent/config.py +5 -8
  477. package/server/services/cli_agent/factory.py +168 -22
  478. package/server/services/cli_agent/jsonl_watcher.py +380 -0
  479. package/server/services/cli_agent/lockfile.py +9 -2
  480. package/server/services/cli_agent/mcp_server.py +110 -34
  481. package/server/services/cli_agent/protocol.py +37 -19
  482. package/server/services/cli_agent/providers/__init__.py +8 -4
  483. package/server/services/cli_agent/providers/google_gemini.py +11 -5
  484. package/server/services/cli_agent/providers/openai_codex.py +34 -34
  485. package/server/services/cli_agent/service.py +245 -83
  486. package/server/services/cli_agent/session.py +409 -229
  487. package/server/services/cli_agent/transports/__init__.py +47 -0
  488. package/server/services/cli_agent/transports/base.py +111 -0
  489. package/server/services/cli_agent/transports/posix.py +196 -0
  490. package/server/services/cli_agent/transports/windows.py +189 -0
  491. package/server/services/cli_agent/types.py +45 -18
  492. package/server/services/cli_agent/workflow_tools.py +28 -15
  493. package/server/services/compaction.py +68 -52
  494. package/server/services/credential_registry.py +6 -20
  495. package/server/services/credentials/__init__.py +18 -0
  496. package/server/services/credentials/handlers.py +196 -0
  497. package/server/services/deployment/__init__.py +12 -1
  498. package/server/services/deployment/canary_registry.py +137 -0
  499. package/server/services/deployment/handlers.py +382 -0
  500. package/server/services/deployment/manager.py +653 -163
  501. package/server/services/deployment/poll_registry.py +2 -6
  502. package/server/services/deployment/state.py +2 -0
  503. package/server/services/deployment/triggers.py +87 -93
  504. package/server/services/event_waiter.py +47 -54
  505. package/server/services/events/__init__.py +11 -0
  506. package/server/services/events/admin_handlers.py +368 -0
  507. package/server/services/events/daemon.py +3 -1
  508. package/server/services/events/dispatch.py +188 -0
  509. package/server/services/events/envelope.py +264 -45
  510. package/server/services/events/oauth_lifecycle.py +98 -42
  511. package/server/services/events/triggers.py +3 -13
  512. package/server/services/events/verifiers/hmac_basic.py +1 -1
  513. package/server/services/events/verifiers/standard_webhooks.py +2 -4
  514. package/server/services/events/webhook.py +2 -3
  515. package/server/services/example_loader.py +73 -15
  516. package/server/services/execution/cache.py +36 -76
  517. package/server/services/execution/conditions.py +7 -20
  518. package/server/services/execution/dlq.py +20 -24
  519. package/server/services/execution/executor.py +234 -265
  520. package/server/services/execution/models.py +40 -46
  521. package/server/services/execution/recovery.py +23 -46
  522. package/server/services/handlers/__init__.py +12 -16
  523. package/server/services/handlers/todo.py +3 -6
  524. package/server/services/handlers/tools.py +143 -194
  525. package/server/services/handlers/triggers.py +24 -23
  526. package/server/services/llm/config.py +10 -1
  527. package/server/services/llm/factory.py +16 -4
  528. package/server/services/llm/messages.py +1 -5
  529. package/server/services/llm/protocol.py +9 -1
  530. package/server/services/llm/providers/anthropic.py +23 -12
  531. package/server/services/llm/providers/gemini.py +43 -22
  532. package/server/services/llm/providers/openai.py +14 -6
  533. package/server/services/llm/providers/openrouter.py +6 -1
  534. package/server/services/markdown_formatter.py +1 -2
  535. package/server/services/memory/__init__.py +2 -2
  536. package/server/services/memory/jsonl.py +6 -2
  537. package/server/services/memory/markdown.py +6 -6
  538. package/server/services/memory/state.py +6 -5
  539. package/server/services/memory_store.py +8 -12
  540. package/server/services/model_registry.py +22 -20
  541. package/server/services/node_executor.py +85 -80
  542. package/server/services/node_output_schemas.py +4 -7
  543. package/server/services/node_registry.py +40 -4
  544. package/server/services/node_spec.py +3 -7
  545. package/server/services/nodejs_client.py +4 -14
  546. package/server/services/oauth_utils.py +11 -7
  547. package/server/services/parameter_resolver.py +30 -36
  548. package/server/services/plugin/base.py +321 -38
  549. package/server/services/plugin/connection.py +12 -7
  550. package/server/services/plugin/credential.py +80 -22
  551. package/server/services/plugin/edge_walker.py +128 -105
  552. package/server/services/plugin/identifiers.py +48 -0
  553. package/server/services/plugin/interceptor.py +1 -1
  554. package/server/services/plugin/oauth.py +25 -21
  555. package/server/services/plugin/operation.py +1 -1
  556. package/server/services/plugin/polling.py +151 -26
  557. package/server/services/plugin/registry.py +52 -4
  558. package/server/services/plugin/routing.py +6 -9
  559. package/server/services/plugin/scaling.py +36 -18
  560. package/server/services/plugin/service_factories.py +95 -0
  561. package/server/services/plugin/shutdown_hooks.py +103 -0
  562. package/server/services/plugin/social_provider_registry.py +80 -0
  563. package/server/services/plugin/ws.py +2 -1
  564. package/server/services/pricing.py +26 -40
  565. package/server/services/pricing_handlers.py +90 -0
  566. package/server/services/process_service.py +33 -32
  567. package/server/services/proxy/models.py +15 -9
  568. package/server/services/proxy/service.py +26 -40
  569. package/server/services/rlm/adapters.py +43 -40
  570. package/server/services/rlm/constants.py +9 -9
  571. package/server/services/rlm/service.py +57 -45
  572. package/server/services/scheduler.py +8 -39
  573. package/server/services/settings/__init__.py +16 -0
  574. package/server/services/settings/handlers.py +275 -0
  575. package/server/services/skill_loader.py +53 -45
  576. package/server/services/skill_prompt.py +8 -6
  577. package/server/services/skills/__init__.py +23 -0
  578. package/server/services/skills/handlers.py +479 -0
  579. package/server/services/status_broadcaster.py +314 -291
  580. package/server/services/temporal/__init__.py +22 -1
  581. package/server/services/temporal/_handlers.py +65 -0
  582. package/server/services/temporal/_install.py +158 -0
  583. package/server/services/temporal/_refresh.py +57 -0
  584. package/server/services/temporal/_retry_policies.py +85 -0
  585. package/server/services/temporal/_runtime.py +181 -0
  586. package/server/services/temporal/_supervised_runtime.py +102 -0
  587. package/server/services/temporal/activities.py +168 -11
  588. package/server/services/temporal/agent_activities.py +683 -0
  589. package/server/services/temporal/agent_workflow.py +601 -0
  590. package/server/services/temporal/client.py +58 -13
  591. package/server/services/temporal/executor.py +2 -3
  592. package/server/services/temporal/plugin_activities.py +37 -2
  593. package/server/services/temporal/plugin_registry.py +82 -0
  594. package/server/services/temporal/polling_trigger_workflow.py +267 -0
  595. package/server/services/temporal/schedules.py +220 -0
  596. package/server/services/temporal/search_attributes.py +177 -0
  597. package/server/services/temporal/trigger_listener_workflow.py +378 -0
  598. package/server/services/temporal/worker.py +111 -18
  599. package/server/services/temporal/workflow.py +259 -40
  600. package/server/services/temporal/ws_client.py +22 -11
  601. package/server/services/text.py +14 -28
  602. package/server/services/tracked_http.py +29 -49
  603. package/server/services/user_auth.py +7 -21
  604. package/server/services/workflow.py +28 -20
  605. package/server/services/workflow_import.py +351 -0
  606. package/server/services/workflow_ops.py +4 -0
  607. package/server/services/workflow_storage/__init__.py +18 -0
  608. package/server/services/workflow_storage/handlers.py +132 -0
  609. package/server/services/workflow_validator.py +209 -0
  610. package/server/services/ws_handler_registry.py +80 -9
  611. package/server/skills/assistant/agent-builder-skill/SKILL.md +6 -6
  612. package/server/tests/conftest.py +54 -3
  613. package/server/tests/credentials/test_auth_service.py +9 -21
  614. package/server/tests/credentials/test_credential_broadcasts.py +116 -22
  615. package/server/tests/credentials/test_credentials_database.py +12 -38
  616. package/server/tests/credentials/test_encryption.py +3 -9
  617. package/server/tests/credentials/test_google_oauth.py +1 -3
  618. package/server/tests/credentials/test_oauth_utils.py +31 -38
  619. package/server/tests/credentials/test_twitter_oauth.py +1 -3
  620. package/server/tests/credentials/test_websocket_handlers.py +37 -72
  621. package/server/tests/fixtures/tool_names_snapshot.json +78 -0
  622. package/server/tests/llm/test_factory.py +12 -4
  623. package/server/tests/llm/test_providers.py +25 -32
  624. package/server/tests/llm/test_wiring.py +27 -22
  625. package/server/tests/nodes/_compat.py +4 -5
  626. package/server/tests/nodes/_harness.py +31 -24
  627. package/server/tests/nodes/_mocks.py +2 -6
  628. package/server/tests/nodes/test_agent_builder.py +43 -35
  629. package/server/tests/nodes/test_ai_agents.py +29 -24
  630. package/server/tests/nodes/test_ai_chat_models.py +3 -9
  631. package/server/tests/nodes/test_ai_tools.py +29 -24
  632. package/server/tests/nodes/test_android.py +34 -64
  633. package/server/tests/nodes/test_chat_utility.py +2 -2
  634. package/server/tests/nodes/test_code_fs_process.py +26 -84
  635. package/server/tests/nodes/test_document.py +23 -47
  636. package/server/tests/nodes/test_email.py +88 -51
  637. package/server/tests/nodes/test_google_workspace.py +26 -20
  638. package/server/tests/nodes/test_http_proxy.py +43 -89
  639. package/server/tests/nodes/test_search.py +3 -9
  640. package/server/tests/nodes/test_specialized_agents.py +58 -162
  641. package/server/tests/nodes/test_stripe_plugin.py +25 -5
  642. package/server/tests/nodes/test_telegram_social.py +33 -37
  643. package/server/tests/nodes/test_twitter.py +59 -150
  644. package/server/tests/nodes/test_web_automation.py +21 -51
  645. package/server/tests/nodes/test_whatsapp.py +13 -19
  646. package/server/tests/nodes/test_workflow_triggers.py +16 -45
  647. package/server/tests/services/cli_agent/test_claude_session_events.py +201 -0
  648. package/server/tests/services/cli_agent/test_jsonl_watcher.py +190 -0
  649. package/server/tests/services/cli_agent/test_mcp_server.py +67 -29
  650. package/server/tests/services/cli_agent/test_providers.py +236 -47
  651. package/server/tests/services/cli_agent/test_service.py +9 -7
  652. package/server/tests/services/memory/test_jsonl.py +30 -25
  653. package/server/tests/services/test_events.py +26 -7
  654. package/server/tests/services/test_identifiers.py +122 -0
  655. package/server/tests/services/test_process_lifecycle.py +129 -0
  656. package/server/tests/services/test_supervisor.py +0 -1
  657. package/server/tests/temporal/__init__.py +0 -0
  658. package/server/tests/temporal/test_agent_workflow.py +215 -0
  659. package/server/tests/temporal/test_dispatch.py +231 -0
  660. package/server/tests/test_admin_handlers.py +394 -0
  661. package/server/tests/test_auto_skill.py +4 -2
  662. package/server/tests/test_canary_registry.py +310 -0
  663. package/server/tests/test_chat_trigger_canary_producer.py +101 -0
  664. package/server/tests/test_cloudevents_node_parameters.py +129 -0
  665. package/server/tests/test_credential_icon.py +115 -0
  666. package/server/tests/test_cron_canary.py +511 -0
  667. package/server/tests/test_deployment_canary_listener.py +692 -0
  668. package/server/tests/test_event_framework_phase_a.py +537 -0
  669. package/server/tests/test_no_raw_prints.py +131 -0
  670. package/server/tests/test_node_spec.py +196 -103
  671. package/server/tests/test_parameter_resolver.py +20 -20
  672. package/server/tests/test_plugin_contract.py +76 -49
  673. package/server/tests/test_plugin_helpers.py +0 -1
  674. package/server/tests/test_plugin_self_containment.py +40 -47
  675. package/server/tests/test_polling_trigger_workflow.py +572 -0
  676. package/server/tests/test_retry_policies.py +146 -0
  677. package/server/tests/test_service_factories.py +168 -0
  678. package/server/tests/test_shutdown_hooks.py +199 -0
  679. package/server/tests/test_social_provider_registry.py +177 -0
  680. package/server/tests/test_status_broadcasts.py +214 -63
  681. package/server/tests/test_task_trigger_canary_producer.py +131 -0
  682. package/server/tests/test_telegram_trigger_canary_producer.py +113 -0
  683. package/server/tests/test_tool_registry.py +110 -0
  684. package/server/tests/test_trigger_listener_workflow.py +365 -0
  685. package/server/tests/test_whatsapp_trigger_canary_producer.py +164 -0
  686. package/server/tests/test_workflow_ops.py +1 -3
  687. package/server/tests/test_workflow_validator.py +791 -0
  688. package/server/uv.lock +3539 -0
  689. package/client/dist/assets/index-DQ0nwhec.js +0 -257
  690. package/client/src/assets/icons/apify/index.ts +0 -19
  691. package/client/src/assets/icons/browser/index.ts +0 -17
  692. package/client/src/assets/icons/email/index.ts +0 -22
  693. package/client/src/assets/icons/google/index.ts +0 -34
  694. package/client/src/assets/icons/llm/deepseek.svg +0 -1
  695. package/client/src/assets/icons/llm/index.ts +0 -18
  696. package/client/src/assets/icons/llm/kimi.svg +0 -1
  697. package/client/src/assets/icons/llm/mistral.svg +0 -1
  698. package/client/src/assets/icons/search/index.ts +0 -28
  699. package/client/src/assets/icons/telegram/index.ts +0 -19
  700. package/machina/buildenv.py +0 -44
  701. package/machina/cli.py +0 -55
  702. package/machina/commands/__init__.py +0 -1
  703. package/machina/commands/clean.py +0 -80
  704. package/machina/commands/daemon.py +0 -150
  705. package/machina/config.py +0 -93
  706. package/machina/platform_.py +0 -37
  707. package/machina/pyproject.toml +0 -33
  708. package/server/nodes/agent/deep_agent.py +0 -103
  709. package/server/services/agents/__init__.py +0 -9
  710. package/server/services/agents/adapters.py +0 -199
  711. package/server/services/agents/constants.py +0 -10
  712. package/server/services/agents/service.py +0 -297
  713. package/server/services/cli_agent/providers/anthropic_claude.py +0 -419
  714. /package/{machina → cli}/README.md +0 -0
  715. /package/{machina → cli}/__init__.py +0 -0
  716. /package/{client/src/assets/icons/apify → server/credentials/icons}/apify.svg +0 -0
  717. /package/{client/src/assets/icons/search/brave.svg → server/credentials/icons/brave_search.svg} +0 -0
  718. /package/{client/src/assets/icons/email/read.svg → server/credentials/icons/email_himalaya.svg} +0 -0
  719. /package/{client/src/assets/icons/search → server/credentials/icons}/perplexity.svg +0 -0
  720. /package/{client/src/assets/icons/search/google.svg → server/credentials/icons/serper.svg} +0 -0
  721. /package/{client/src/assets → server/credentials}/icons/stripe.svg +0 -0
  722. /package/{client/src/assets/icons/twitter/x.svg → server/credentials/icons/twitter.svg} +0 -0
  723. /package/{client/src/assets/icons/browser/chrome.svg → server/nodes/browser/browser/icon.svg} +0 -0
  724. /package/{client/src/assets/icons/chat/chat.svg → server/nodes/chat/chat_history/icon.svg} +0 -0
  725. /package/{client/src/assets/icons/code/javascript.svg → server/nodes/code/javascript_executor/icon.svg} +0 -0
  726. /package/{client/src/assets/icons/code/python.svg → server/nodes/code/python_executor/icon.svg} +0 -0
  727. /package/{client/src/assets/icons/code/typescript.svg → server/nodes/code/typescript_executor/icon.svg} +0 -0
  728. /package/{client/src/assets/icons/email/receive.svg → server/nodes/email/email_receive/icon.svg} +0 -0
  729. /package/{client/src/assets/icons/email/send.svg → server/nodes/email/email_send/icon.svg} +0 -0
  730. /package/{client/src/assets/icons/google/calendar.svg → server/nodes/google/calendar/icon.svg} +0 -0
  731. /package/{client/src/assets/icons/google/contacts.svg → server/nodes/google/contacts/icon.svg} +0 -0
  732. /package/{client/src/assets/icons/google/drive.svg → server/nodes/google/drive/icon.svg} +0 -0
  733. /package/{client/src/assets/icons/google/gmail.svg → server/nodes/google/gmail/icon.svg} +0 -0
  734. /package/{client/src/assets/icons/google/sheets.svg → server/nodes/google/sheets/icon.svg} +0 -0
  735. /package/{client/src/assets/icons/google/tasks.svg → server/nodes/google/tasks/icon.svg} +0 -0
  736. /package/{client/src/assets/icons/search/duckduckgo.svg → server/nodes/search/duckduckgo_search/icon.svg} +0 -0
  737. /package/{client/src/assets/icons/social/social.svg → server/nodes/social/social_receive/icon.svg} +0 -0
  738. /package/{client/src/assets/icons/telegram/telegram.svg → server/nodes/telegram/icon.svg} +0 -0
  739. /package/server/nodes/utility/{team_monitor.py → team_monitor/__init__.py} +0 -0
  740. /package/{client/src/assets/icons/whatsapp/whatsapp-db.svg → server/nodes/whatsapp/icon_whatsappDb.svg} +0 -0
  741. /package/{client/src/assets/icons/whatsapp/whatsapp-receive.svg → server/nodes/whatsapp/icon_whatsappReceive.svg} +0 -0
  742. /package/{client/src/assets/icons/whatsapp/whatsapp-send.svg → server/nodes/whatsapp/icon_whatsappSend.svg} +0 -0
  743. /package/{client/src/assets/icons → server/nodes}/whatsapp/whatsapp.svg +0 -0
@@ -0,0 +1,791 @@
1
+ """Contract tests for ``services.workflow_validator.validate_workflow``.
2
+
3
+ Locks the six issue codes the validator surfaces:
4
+
5
+ - ``DANGLING_EDGE`` — edge references a node id that isn't in the graph.
6
+ - ``UNKNOWN_NODE_TYPE`` — plugin not installed on this instance.
7
+ - ``INVALID_PARAM`` — Pydantic ``Params.model_validate`` raises.
8
+ - ``MISSING_CREDENTIAL`` — declared credential not stored.
9
+ - ``CYCLE`` — Kahn's algorithm leaves nodes unresolved.
10
+ - Empty report — valid workflow returns ``{errors: [], warnings: []}``.
11
+
12
+ Used to gate ``handle_execute_workflow`` (force=False), all
13
+ ``handle_deploy_workflow`` calls, and ``example_loader.import_examples_for_user``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from unittest.mock import AsyncMock, MagicMock
19
+
20
+ import pytest
21
+ from pydantic import BaseModel, Field
22
+
23
+
24
+ pytestmark = pytest.mark.asyncio
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Fakes — a small in-memory plugin registry so tests are deterministic and
29
+ # don't depend on which plugins happen to be installed at run time.
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ class _FakeCredential:
34
+ id = "fake_provider"
35
+ auth = "api_key"
36
+
37
+
38
+ class _FakeParams(BaseModel):
39
+ text: str = Field(..., min_length=1)
40
+
41
+
42
+ class _FakeNodeClass:
43
+ type = "fakeNode"
44
+ Params = _FakeParams
45
+ credentials = (_FakeCredential,)
46
+
47
+
48
+ class _FakeNodelessClass:
49
+ """Plugin with no credentials and a permissive Params (every workflow
50
+ invariant is honored)."""
51
+
52
+ type = "fakeNodeless"
53
+
54
+ class Params(BaseModel):
55
+ pass
56
+
57
+ credentials = ()
58
+
59
+
60
+ def _patch_registry(monkeypatch, mapping: dict[str, object]) -> None:
61
+ """Patch ``get_node_class`` to return our fakes."""
62
+ monkeypatch.setattr(
63
+ "services.workflow_validator.get_node_class",
64
+ lambda node_type: mapping.get(node_type),
65
+ )
66
+
67
+
68
+ def _patch_auth(monkeypatch, has_valid_key_return: bool) -> None:
69
+ """Patch the container so ``auth_service.has_valid_key`` returns a fixed bool."""
70
+ fake_auth = MagicMock()
71
+ fake_auth.has_valid_key = AsyncMock(return_value=has_valid_key_return)
72
+ fake_container = MagicMock()
73
+ fake_container.auth_service.return_value = fake_auth
74
+ monkeypatch.setattr("core.container.container", fake_container)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Tests
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ async def test_dangling_edge_target_is_error(monkeypatch):
83
+ from services.workflow_validator import validate_workflow
84
+
85
+ _patch_registry(monkeypatch, {"fakeNodeless": _FakeNodelessClass})
86
+ _patch_auth(monkeypatch, True)
87
+
88
+ report = await validate_workflow(
89
+ nodes=[{"id": "n1", "type": "fakeNodeless", "data": {}}],
90
+ edges=[{"id": "e1", "source": "n1", "target": "ghost"}],
91
+ )
92
+ codes = [iss["code"] for iss in report["errors"]]
93
+ assert "DANGLING_EDGE" in codes
94
+
95
+
96
+ async def test_dangling_edge_source_is_error(monkeypatch):
97
+ from services.workflow_validator import validate_workflow
98
+
99
+ _patch_registry(monkeypatch, {"fakeNodeless": _FakeNodelessClass})
100
+ _patch_auth(monkeypatch, True)
101
+
102
+ report = await validate_workflow(
103
+ nodes=[{"id": "n1", "type": "fakeNodeless", "data": {}}],
104
+ edges=[{"id": "e1", "source": "ghost", "target": "n1"}],
105
+ )
106
+ codes = [iss["code"] for iss in report["errors"]]
107
+ assert "DANGLING_EDGE" in codes
108
+
109
+
110
+ async def test_unknown_node_type_is_error(monkeypatch):
111
+ from services.workflow_validator import validate_workflow
112
+
113
+ _patch_registry(monkeypatch, {})
114
+ _patch_auth(monkeypatch, True)
115
+
116
+ report = await validate_workflow(
117
+ nodes=[{"id": "n1", "type": "doesNotExist", "data": {}}],
118
+ edges=[],
119
+ )
120
+ codes = [iss["code"] for iss in report["errors"]]
121
+ assert "UNKNOWN_NODE_TYPE" in codes
122
+ issue = next(iss for iss in report["errors"] if iss["code"] == "UNKNOWN_NODE_TYPE")
123
+ assert issue["node_id"] == "n1"
124
+ assert issue["node_type"] == "doesNotExist"
125
+
126
+
127
+ async def test_invalid_param_is_warning(monkeypatch):
128
+ """INVALID_PARAM is a WARNING — matches the runtime soft-fail at
129
+ ``node_executor._prepare_parameters`` (logs WARN, continues)."""
130
+ from services.workflow_validator import validate_workflow
131
+
132
+ _patch_registry(monkeypatch, {"fakeNode": _FakeNodeClass})
133
+ _patch_auth(monkeypatch, True)
134
+
135
+ # text is required + min_length=1; empty triggers ValidationError.
136
+ report = await validate_workflow(
137
+ nodes=[{"id": "n1", "type": "fakeNode", "data": {"parameters": {"text": ""}}}],
138
+ edges=[],
139
+ )
140
+ codes = [iss["code"] for iss in report["warnings"]]
141
+ assert "INVALID_PARAM" in codes
142
+ assert all(
143
+ iss["code"] != "INVALID_PARAM" for iss in report["errors"]
144
+ ), "INVALID_PARAM must not block execution; only deploy-time errors block."
145
+
146
+
147
+ async def test_missing_credential_is_warning(monkeypatch):
148
+ """MISSING_CREDENTIAL is a WARNING so the workflow can be saved/imported
149
+ and credentials configured afterward. Runtime failure (different path,
150
+ different broadcast) is what surfaces to the user during execution."""
151
+ from services.workflow_validator import validate_workflow
152
+
153
+ _patch_registry(monkeypatch, {"fakeNode": _FakeNodeClass})
154
+ _patch_auth(monkeypatch, False) # credential NOT stored
155
+
156
+ report = await validate_workflow(
157
+ nodes=[{"id": "n1", "type": "fakeNode", "data": {"parameters": {"text": "ok"}}}],
158
+ edges=[],
159
+ )
160
+ codes = [iss["code"] for iss in report["warnings"]]
161
+ assert "MISSING_CREDENTIAL" in codes
162
+ issue = next(iss for iss in report["warnings"] if iss["code"] == "MISSING_CREDENTIAL")
163
+ assert issue["provider_id"] == "fake_provider"
164
+ assert issue["remediation"] == "add_key"
165
+ assert issue["node_id"] == "n1"
166
+
167
+
168
+ async def test_cycle_is_error(monkeypatch):
169
+ from services.workflow_validator import validate_workflow
170
+
171
+ _patch_registry(monkeypatch, {"fakeNodeless": _FakeNodelessClass})
172
+ _patch_auth(monkeypatch, True)
173
+
174
+ # Two-node cycle: n1 -> n2 -> n1
175
+ report = await validate_workflow(
176
+ nodes=[
177
+ {"id": "n1", "type": "fakeNodeless", "data": {}},
178
+ {"id": "n2", "type": "fakeNodeless", "data": {}},
179
+ ],
180
+ edges=[
181
+ {"id": "e1", "source": "n1", "target": "n2"},
182
+ {"id": "e2", "source": "n2", "target": "n1"},
183
+ ],
184
+ )
185
+ codes = [iss["code"] for iss in report["errors"]]
186
+ assert "CYCLE" in codes
187
+ cycle = next(iss for iss in report["errors"] if iss["code"] == "CYCLE")
188
+ assert set(cycle["nodes"]) == {"n1", "n2"}
189
+
190
+
191
+ async def test_valid_workflow_returns_empty_report(monkeypatch):
192
+ from services.workflow_validator import validate_workflow
193
+
194
+ _patch_registry(monkeypatch, {"fakeNodeless": _FakeNodelessClass})
195
+ _patch_auth(monkeypatch, True)
196
+
197
+ report = await validate_workflow(
198
+ nodes=[
199
+ {"id": "n1", "type": "fakeNodeless", "data": {}},
200
+ {"id": "n2", "type": "fakeNodeless", "data": {}},
201
+ ],
202
+ edges=[{"id": "e1", "source": "n1", "target": "n2"}],
203
+ )
204
+ assert report == {"errors": [], "warnings": []}
205
+
206
+
207
+ async def test_parameters_by_id_overrides_node_data(monkeypatch):
208
+ """When parameters_by_id is supplied, it wins over node.data.parameters.
209
+ Used by the WS execute handler (hydrating from DB) and the
210
+ example_loader (which holds params in the JSON's nodeParameters block)."""
211
+ from services.workflow_validator import validate_workflow
212
+
213
+ _patch_registry(monkeypatch, {"fakeNode": _FakeNodeClass})
214
+ _patch_auth(monkeypatch, True)
215
+
216
+ # node.data has valid params, parameters_by_id has invalid → invalid wins.
217
+ report = await validate_workflow(
218
+ nodes=[{"id": "n1", "type": "fakeNode", "data": {"parameters": {"text": "good"}}}],
219
+ edges=[],
220
+ parameters_by_id={"n1": {"text": ""}},
221
+ )
222
+ codes = [iss["code"] for iss in report["warnings"]]
223
+ assert "INVALID_PARAM" in codes
224
+
225
+
226
+ async def test_report_shape_is_plain_dicts(monkeypatch):
227
+ """Issues must be plain dicts (no dataclasses / enums) so the report
228
+ serializes directly to JSON without custom encoders."""
229
+ import json
230
+
231
+ from services.workflow_validator import validate_workflow
232
+
233
+ _patch_registry(monkeypatch, {})
234
+ _patch_auth(monkeypatch, True)
235
+
236
+ report = await validate_workflow(
237
+ nodes=[{"id": "n1", "type": "unknown", "data": {}}],
238
+ edges=[],
239
+ )
240
+ # Should round-trip through stdlib JSON without errors.
241
+ serialized = json.dumps(report)
242
+ assert "UNKNOWN_NODE_TYPE" in serialized
243
+
244
+
245
+ # ---------------------------------------------------------------------------
246
+ # WS-handler gating invariants
247
+ # ---------------------------------------------------------------------------
248
+
249
+
250
+ class TestExecuteAndDeployHandlersGate:
251
+ """Static-source contract: ``handle_execute_workflow`` and
252
+ ``handle_deploy_workflow`` MUST call ``validate_workflow`` before
253
+ handing the graph off to ``WorkflowService``. Anchor on the function
254
+ name string so a rename forces a deliberate test update.
255
+ """
256
+
257
+ @staticmethod
258
+ def _handler_source(handler) -> str:
259
+ import inspect
260
+
261
+ fn = handler
262
+ while hasattr(fn, "__wrapped__"):
263
+ fn = fn.__wrapped__
264
+ return inspect.getsource(fn)
265
+
266
+ def test_execute_workflow_calls_validator(self):
267
+ from routers import websocket as ws_module
268
+
269
+ src = self._handler_source(ws_module.handle_execute_workflow)
270
+ assert "validate_workflow" in src, (
271
+ "handle_execute_workflow must call validate_workflow so " "broken workflows are blocked at the gate (force=true overrides)."
272
+ )
273
+ assert "force" in src, (
274
+ "handle_execute_workflow must support the force=true override " "so 'Run anyway' can bypass warnings (Windmill pattern)."
275
+ )
276
+
277
+ def test_deploy_workflow_calls_validator_unconditionally(self):
278
+ # Wave 13.2: handle_deploy_workflow moved out of routers.websocket
279
+ # into services/deployment/handlers.py.
280
+ from services.deployment.handlers import handle_deploy_workflow
281
+
282
+ src = self._handler_source(handle_deploy_workflow)
283
+ assert "validate_workflow" in src, (
284
+ "handle_deploy_workflow must call validate_workflow — a broken "
285
+ "workflow deployed on a schedule is much worse than a failed "
286
+ "manual run, so deploy never honors a force-override."
287
+ )
288
+
289
+ def test_validate_workflow_handler_registered(self):
290
+ from routers import websocket as ws_module
291
+
292
+ assert "validate_workflow" in ws_module.MESSAGE_HANDLERS, (
293
+ "validate_workflow message type must be in MESSAGE_HANDLERS " "so the frontend live-lint and import dry-run paths can reach it."
294
+ )
295
+
296
+ def test_example_loader_calls_validator(self):
297
+ """First-launch example import must run validator to skip
298
+ malformed examples (errors) and log credential gaps (warnings)."""
299
+ import inspect
300
+ from services import example_loader
301
+
302
+ src = inspect.getsource(example_loader.import_examples_for_user)
303
+ assert "validate_workflow" in src, (
304
+ "import_examples_for_user must run validate_workflow before " "save_workflow — broken examples shipped on disk are bugs."
305
+ )
306
+
307
+ def test_example_loader_remaps_node_ids(self):
308
+ """Example loader MUST rewrite node ids before save. Without this,
309
+ two example workflows that share a node id (today: AI Assistant +
310
+ Claude Assistant share 6 trigger/console/memory nodes) collide in
311
+ the node_parameters table (keyed unique on node_id), and the
312
+ second example silently overwrites the first's parameters."""
313
+ import inspect
314
+ from services import example_loader
315
+
316
+ src = inspect.getsource(example_loader.import_examples_for_user)
317
+ assert "remap_node_ids" in src, (
318
+ "import_examples_for_user must call workflow_import.remap_node_ids " "before save_workflow / save_node_parameters."
319
+ )
320
+
321
+
322
+ class TestRemapNodeIds:
323
+ """Regression contract for ``services.workflow_import.remap_node_ids``.
324
+ Two example workflows in ``workflows/`` share 6 node ids — this helper
325
+ is what prevents parameter overwrites at import time."""
326
+
327
+ def test_remap_eliminates_duplicate_ids(self):
328
+ """Two graphs with identical input ids must emit disjoint output
329
+ id sets after independent remap calls."""
330
+ from services.workflow_import import remap_node_ids
331
+
332
+ nodes_a = [{"id": "shared-1", "type": "telegramSend", "data": {}}]
333
+ nodes_b = [{"id": "shared-1", "type": "telegramSend", "data": {}}]
334
+ a_nodes, _, _ = remap_node_ids(nodes_a, [], {"shared-1": {"x": 1}})
335
+ b_nodes, _, _ = remap_node_ids(nodes_b, [], {"shared-1": {"x": 2}})
336
+
337
+ a_ids = {n["id"] for n in a_nodes}
338
+ b_ids = {n["id"] for n in b_nodes}
339
+ assert a_ids.isdisjoint(b_ids), f"Remap must produce disjoint id sets, got overlap: {a_ids & b_ids}"
340
+
341
+ def test_remap_rewrites_edge_refs(self):
342
+ """Edges must point at the new node ids; no dangling references."""
343
+ from services.workflow_import import remap_node_ids
344
+
345
+ nodes = [
346
+ {"id": "n1", "type": "start", "data": {}},
347
+ {"id": "n2", "type": "httpRequest", "data": {}},
348
+ ]
349
+ edges = [{"id": "e1", "source": "n1", "target": "n2"}]
350
+ new_nodes, new_edges, _ = remap_node_ids(nodes, edges, {})
351
+
352
+ node_id_set = {n["id"] for n in new_nodes}
353
+ assert len(new_edges) == 1
354
+ assert new_edges[0]["source"] in node_id_set
355
+ assert new_edges[0]["target"] in node_id_set
356
+
357
+ def test_remap_rekeys_parameters(self):
358
+ """nodeParameters keys must be remapped to match the new node ids."""
359
+ from services.workflow_import import remap_node_ids
360
+
361
+ nodes = [{"id": "orig-1", "type": "telegramSend", "data": {}}]
362
+ new_nodes, _, new_params = remap_node_ids(nodes, [], {"orig-1": {"text": "hello"}})
363
+ assert "orig-1" not in new_params
364
+ assert new_nodes[0]["id"] in new_params
365
+ assert new_params[new_nodes[0]["id"]] == {"text": "hello"}
366
+
367
+ def test_remap_drops_orphan_parameters(self):
368
+ """A parameter entry whose node isn't in the graph is dead data
369
+ (rare, only happens when an export was hand-edited). Dropped."""
370
+ from services.workflow_import import remap_node_ids
371
+
372
+ _, _, new_params = remap_node_ids(
373
+ [{"id": "n1", "type": "start", "data": {}}],
374
+ [],
375
+ {"n1": {"x": 1}, "ghost": {"y": 2}},
376
+ )
377
+ assert len(new_params) == 1, "orphan parameter entry should be dropped"
378
+
379
+ def test_remap_real_examples_no_collisions(self):
380
+ """Spot check against the actual example workflow files shipped in
381
+ ``workflows/``: today AI Assistant + Claude Assistant share 6
382
+ node ids; after remap, the union must be collision-free."""
383
+ import collections
384
+ import glob
385
+ import json
386
+
387
+ from core.paths import example_workflows_dir
388
+ from services.workflow_import import remap_node_ids
389
+
390
+ examples_dir = example_workflows_dir()
391
+ all_ids = collections.defaultdict(list)
392
+ for path in sorted(glob.glob(str(examples_dir / "*.json"))):
393
+ with open(path, encoding="utf-8") as fh:
394
+ wf = json.load(fh)
395
+ nodes, _, _ = remap_node_ids(
396
+ wf.get("nodes", []),
397
+ wf.get("edges", []),
398
+ wf.get("nodeParameters", {}),
399
+ )
400
+ for node in nodes:
401
+ all_ids[node["id"]].append(path)
402
+
403
+ collisions = {k: v for k, v in all_ids.items() if len(v) > 1}
404
+ assert not collisions, f"Remap should produce disjoint ids across all example " f"workflows; found collisions: {collisions}"
405
+
406
+
407
+ # ---------------------------------------------------------------------------
408
+ # workflow_import orchestrator tests
409
+ # ---------------------------------------------------------------------------
410
+
411
+
412
+ class _FakeWorkflow:
413
+ """Mimics the SQLModel.Workflow rows returned by get_all_workflows."""
414
+
415
+ def __init__(self, name: str):
416
+ self.name = name
417
+
418
+
419
+ def _fake_database(existing_names: list[str] | None = None):
420
+ """In-memory database double for the orchestrator tests."""
421
+ saved_workflows: list[dict] = []
422
+ saved_params: dict[str, dict] = {}
423
+
424
+ db = MagicMock()
425
+ db.get_all_workflows = AsyncMock(return_value=[_FakeWorkflow(n) for n in (existing_names or [])])
426
+
427
+ async def save_workflow(**kwargs):
428
+ saved_workflows.append(kwargs)
429
+ return True
430
+
431
+ async def save_node_parameters(node_id, params):
432
+ saved_params[node_id] = params
433
+ return True
434
+
435
+ db.save_workflow = AsyncMock(side_effect=save_workflow)
436
+ db.save_node_parameters = AsyncMock(side_effect=save_node_parameters)
437
+ db._saved_workflows = saved_workflows # test-readable
438
+ db._saved_params = saved_params
439
+ return db
440
+
441
+
442
+ def _fake_auth(has_valid_key_return: bool = True):
443
+ auth = MagicMock()
444
+ auth.has_valid_key = AsyncMock(return_value=has_valid_key_return)
445
+ return auth
446
+
447
+
448
+ class TestImportWorkflowOrchestrator:
449
+ """Contract for ``services.workflow_import.import_workflow``. Locks the
450
+ two-step UX: first call returns preview when confirmations are needed
451
+ (name conflict / missing creds), second call with confirmations saves."""
452
+
453
+ async def test_save_path_with_no_conflicts_no_missing_creds(self, monkeypatch):
454
+ """Happy path: valid workflow, no name conflict, no missing creds
455
+ → preview is False, workflow saved, workflow_id returned."""
456
+ from services.workflow_import import import_workflow
457
+
458
+ # Patch the registry so the synthetic node type validates.
459
+ _patch_registry_in_validator(monkeypatch)
460
+
461
+ db = _fake_database(existing_names=[])
462
+ auth = _fake_auth(has_valid_key_return=True)
463
+
464
+ result = await import_workflow(
465
+ {
466
+ "name": "My Workflow",
467
+ "nodes": [{"id": "n1", "type": "fakeNodeless", "data": {}}],
468
+ "edges": [],
469
+ },
470
+ auth_service=auth,
471
+ database=db,
472
+ )
473
+ assert result["success"] is True
474
+ assert result["preview"] is False
475
+ assert result["workflow_id"].startswith("workflow-")
476
+ assert result["name"] == "My Workflow"
477
+ assert len(db._saved_workflows) == 1
478
+
479
+ async def test_preview_when_name_conflicts(self, monkeypatch):
480
+ """Name conflict → preview=True with suggested_name; no save yet."""
481
+ from services.workflow_import import import_workflow
482
+
483
+ _patch_registry_in_validator(monkeypatch)
484
+ db = _fake_database(existing_names=["My Workflow"])
485
+ auth = _fake_auth(has_valid_key_return=True)
486
+
487
+ result = await import_workflow(
488
+ {
489
+ "name": "My Workflow",
490
+ "nodes": [{"id": "n1", "type": "fakeNodeless", "data": {}}],
491
+ "edges": [],
492
+ },
493
+ auth_service=auth,
494
+ database=db,
495
+ )
496
+ assert result["success"] is True
497
+ assert result["preview"] is True
498
+ assert result["name_conflict"] is True
499
+ assert result["suggested_name"] == "My Workflow (imported)"
500
+ assert len(db._saved_workflows) == 0 # NOT saved yet
501
+
502
+ async def test_preview_when_credentials_missing(self, monkeypatch):
503
+ """Missing credentials (without force flag) → preview=True with
504
+ missing_credentials list; no save yet."""
505
+ from services.workflow_import import import_workflow
506
+
507
+ _patch_registry_in_validator(monkeypatch)
508
+ # Patch CREDENTIAL_REGISTRY so cross-check can pull display info.
509
+ monkeypatch.setattr(
510
+ "services.plugin.credential.CREDENTIAL_REGISTRY",
511
+ {"fake_provider": _FakeCredential},
512
+ )
513
+ db = _fake_database(existing_names=[])
514
+ auth = _fake_auth(has_valid_key_return=False) # no creds stored
515
+
516
+ result = await import_workflow(
517
+ {
518
+ "name": "Need Creds",
519
+ "nodes": [{"id": "n1", "type": "fakeNode", "data": {"parameters": {"text": "ok"}}}],
520
+ "edges": [],
521
+ },
522
+ auth_service=auth,
523
+ database=db,
524
+ )
525
+ assert result["preview"] is True
526
+ assert len(result["missing_credentials"]) == 1
527
+ assert result["missing_credentials"][0]["provider_id"] == "fake_provider"
528
+ assert len(db._saved_workflows) == 0
529
+
530
+ async def test_force_credentials_skips_preview(self, monkeypatch):
531
+ """force_credentials=True bypasses the missing-credential preview."""
532
+ from services.workflow_import import import_workflow
533
+
534
+ _patch_registry_in_validator(monkeypatch)
535
+ monkeypatch.setattr(
536
+ "services.plugin.credential.CREDENTIAL_REGISTRY",
537
+ {"fake_provider": _FakeCredential},
538
+ )
539
+ # Mock the broadcaster — save path emits workflow.imported.
540
+ _patch_broadcaster(monkeypatch)
541
+ db = _fake_database(existing_names=[])
542
+ auth = _fake_auth(has_valid_key_return=False)
543
+
544
+ result = await import_workflow(
545
+ {
546
+ "name": "Force",
547
+ "nodes": [{"id": "n1", "type": "fakeNode", "data": {"parameters": {"text": "ok"}}}],
548
+ "edges": [],
549
+ },
550
+ force_credentials=True,
551
+ auth_service=auth,
552
+ database=db,
553
+ )
554
+ assert result["success"] is True
555
+ assert result["preview"] is False
556
+ assert len(db._saved_workflows) == 1
557
+
558
+ async def test_validation_errors_block_import(self, monkeypatch):
559
+ """Validation errors (e.g. unknown node type, cycle) short-circuit
560
+ with success=False — no preview, no save."""
561
+ from services.workflow_import import import_workflow
562
+
563
+ _patch_registry_in_validator(monkeypatch)
564
+ db = _fake_database()
565
+ auth = _fake_auth()
566
+
567
+ result = await import_workflow(
568
+ {
569
+ "name": "Bad",
570
+ "nodes": [{"id": "n1", "type": "doesNotExist", "data": {}}],
571
+ "edges": [],
572
+ },
573
+ auth_service=auth,
574
+ database=db,
575
+ )
576
+ assert result["success"] is False
577
+ assert result["error"] == "validation_failed"
578
+ assert any(i["code"] == "UNKNOWN_NODE_TYPE" for i in result["report"]["errors"])
579
+ assert len(db._saved_workflows) == 0
580
+
581
+ async def test_save_path_remaps_node_ids(self, monkeypatch):
582
+ """Save path applies remap_node_ids so the saved workflow has
583
+ fresh ids regardless of the input. Two saves of the same JSON
584
+ produce disjoint id sets."""
585
+ from services.workflow_import import import_workflow
586
+
587
+ _patch_registry_in_validator(monkeypatch)
588
+ _patch_broadcaster(monkeypatch)
589
+ db = _fake_database(existing_names=[])
590
+ auth = _fake_auth(has_valid_key_return=True)
591
+
592
+ payload = {
593
+ "name": "Remap A",
594
+ "nodes": [{"id": "orig-1", "type": "fakeNodeless", "data": {}}],
595
+ "edges": [],
596
+ "nodeParameters": {"orig-1": {"x": 1}},
597
+ }
598
+ r1 = await import_workflow(payload, auth_service=auth, database=db)
599
+ r2 = await import_workflow({**payload, "name": "Remap B"}, auth_service=auth, database=db)
600
+
601
+ # Saved node ids must differ across the two imports.
602
+ saved_a_nodes = db._saved_workflows[0]["data"]["nodes"]
603
+ saved_b_nodes = db._saved_workflows[1]["data"]["nodes"]
604
+ a_ids = {n["id"] for n in saved_a_nodes}
605
+ b_ids = {n["id"] for n in saved_b_nodes}
606
+ assert a_ids.isdisjoint(b_ids), f"Two imports of the same JSON must produce disjoint node ids; " f"overlap: {a_ids & b_ids}"
607
+ # Parameters saved under the NEW ids, not the original.
608
+ assert "orig-1" not in db._saved_params
609
+ assert len(db._saved_params) >= 2 # both r1 and r2 saved their params
610
+ # Sanity: r1 and r2 both succeeded.
611
+ assert r1["success"] and r2["success"]
612
+
613
+ async def test_save_path_broadcasts_workflow_imported(self, monkeypatch):
614
+ """Save path must emit a CloudEvents-typed workflow.imported event
615
+ via broadcast_workflow_lifecycle. Other connected clients listen
616
+ for this and invalidate the workflows query."""
617
+ from services.workflow_import import import_workflow
618
+
619
+ _patch_registry_in_validator(monkeypatch)
620
+ broadcaster, calls = _patch_broadcaster(monkeypatch)
621
+ db = _fake_database()
622
+ auth = _fake_auth()
623
+
624
+ await import_workflow(
625
+ {
626
+ "name": "Broadcast Test",
627
+ "nodes": [{"id": "n1", "type": "fakeNodeless", "data": {}}],
628
+ "edges": [],
629
+ },
630
+ auth_service=auth,
631
+ database=db,
632
+ )
633
+ assert len(calls) == 1, "import_workflow save path must call broadcast_workflow_lifecycle " "exactly once on success."
634
+ stage, kwargs = calls[0]
635
+ assert stage == "imported"
636
+ assert kwargs["workflow_id"].startswith("workflow-")
637
+ assert kwargs["name"] == "Broadcast Test"
638
+
639
+
640
+ def _patch_registry_in_validator(monkeypatch):
641
+ """Patch ``services.workflow_validator.get_node_class`` (the lookup
642
+ used by the validator)."""
643
+ registry = {"fakeNodeless": _FakeNodelessClass, "fakeNode": _FakeNodeClass}
644
+ monkeypatch.setattr(
645
+ "services.workflow_validator.get_node_class",
646
+ lambda t: registry.get(t),
647
+ )
648
+ # workflow_import.extract_requirements imports get_node_class too.
649
+ monkeypatch.setattr(
650
+ "services.workflow_import.get_node_class",
651
+ lambda t: registry.get(t),
652
+ )
653
+
654
+
655
+ def _patch_broadcaster(monkeypatch):
656
+ """Replace get_status_broadcaster with a stub that records lifecycle
657
+ calls so tests can assert on them without touching real WebSockets."""
658
+ calls: list[tuple[str, dict]] = []
659
+
660
+ class _StubBroadcaster:
661
+ async def broadcast_workflow_lifecycle(self, stage: str, **kwargs):
662
+ calls.append((stage, kwargs))
663
+
664
+ stub = _StubBroadcaster()
665
+ monkeypatch.setattr(
666
+ "services.status_broadcaster.get_status_broadcaster",
667
+ lambda: stub,
668
+ )
669
+ return stub, calls
670
+
671
+
672
+ # ---------------------------------------------------------------------------
673
+ # extract_requirements + cross_check_credentials + check_name_conflict
674
+ # ---------------------------------------------------------------------------
675
+
676
+
677
+ class TestExtractRequirements:
678
+ def test_pulls_credential_ids_and_node_versions(self, monkeypatch):
679
+ from services.workflow_import import extract_requirements
680
+
681
+ monkeypatch.setattr(
682
+ "services.workflow_import.get_node_class",
683
+ lambda t: {
684
+ "fakeNode": _FakeNodeClass,
685
+ "fakeNodeless": _FakeNodelessClass,
686
+ }.get(t),
687
+ )
688
+
689
+ reqs = extract_requirements(
690
+ [
691
+ {"id": "n1", "type": "fakeNode"},
692
+ {"id": "n2", "type": "fakeNodeless"},
693
+ ]
694
+ )
695
+ cred_ids = [c["provider_id"] for c in reqs["credentials"]]
696
+ types = [n["type"] for n in reqs["nodes"]]
697
+ assert "fake_provider" in cred_ids
698
+ assert "fakeNode" in types
699
+ assert "fakeNodeless" in types
700
+
701
+ def test_ignores_unknown_types(self, monkeypatch):
702
+ from services.workflow_import import extract_requirements
703
+
704
+ monkeypatch.setattr(
705
+ "services.workflow_import.get_node_class",
706
+ lambda t: None,
707
+ )
708
+ reqs = extract_requirements([{"id": "n1", "type": "unknown"}])
709
+ assert reqs == {"credentials": [], "nodes": []}
710
+
711
+
712
+ class TestCrossCheckCredentials:
713
+ async def test_reports_missing_with_display_info(self, monkeypatch):
714
+ from services.workflow_import import cross_check_credentials
715
+
716
+ monkeypatch.setattr(
717
+ "services.plugin.credential.CREDENTIAL_REGISTRY",
718
+ {"fake_provider": _FakeCredential},
719
+ )
720
+ auth = _fake_auth(has_valid_key_return=False)
721
+
722
+ missing = await cross_check_credentials({"credentials": [{"provider_id": "fake_provider"}]}, auth)
723
+ assert len(missing) == 1
724
+ assert missing[0]["provider_id"] == "fake_provider"
725
+ assert "display_name" in missing[0]
726
+ assert missing[0]["kind"] == "api_key"
727
+
728
+ async def test_empty_when_all_stored(self, monkeypatch):
729
+ from services.workflow_import import cross_check_credentials
730
+
731
+ auth = _fake_auth(has_valid_key_return=True)
732
+ missing = await cross_check_credentials({"credentials": [{"provider_id": "x"}]}, auth)
733
+ assert missing == []
734
+
735
+
736
+ class TestCheckNameConflict:
737
+ async def test_detects_conflict_and_suggests_name(self):
738
+ from services.workflow_import import check_name_conflict
739
+
740
+ db = _fake_database(existing_names=["My Flow", "Other"])
741
+ result = await check_name_conflict("My Flow", db)
742
+ assert result["has_conflict"] is True
743
+ assert result["suggested_name"] == "My Flow (imported)"
744
+
745
+ async def test_increments_suffix_when_suggestion_also_conflicts(self):
746
+ from services.workflow_import import check_name_conflict
747
+
748
+ db = _fake_database(existing_names=["A", "A (imported)"])
749
+ result = await check_name_conflict("A", db)
750
+ assert result["suggested_name"] == "A (imported) 2"
751
+
752
+ async def test_no_conflict_returns_none(self):
753
+ from services.workflow_import import check_name_conflict
754
+
755
+ db = _fake_database(existing_names=["Other"])
756
+ result = await check_name_conflict("Unique", db)
757
+ assert result["has_conflict"] is False
758
+ assert result["suggested_name"] is None
759
+
760
+
761
+ class TestCloudEventsBroadcastShape:
762
+ """The save path emits a real WorkflowEvent envelope. Static-source
763
+ check that the broadcaster method uses the typed factory."""
764
+
765
+ def test_broadcaster_uses_workflow_lifecycle_factory(self):
766
+ import inspect
767
+
768
+ from services import status_broadcaster
769
+
770
+ src = inspect.getsource(status_broadcaster.StatusBroadcaster.broadcast_workflow_lifecycle)
771
+ assert "WorkflowEvent.workflow_lifecycle" in src, (
772
+ "broadcast_workflow_lifecycle must build the envelope via the "
773
+ "typed factory so the dataschema URI + reverse-DNS type prefix "
774
+ "are consistent with the rest of the CloudEvents surface."
775
+ )
776
+ assert '"workflow_lifecycle"' in src, (
777
+ "wire-format key must be 'workflow_lifecycle' so the frontend " "WebSocketContext.tsx case statement routes correctly."
778
+ )
779
+
780
+ def test_import_workflow_emits_imported_lifecycle(self):
781
+ import inspect
782
+
783
+ from services import workflow_import
784
+
785
+ src = inspect.getsource(workflow_import.import_workflow)
786
+ assert "broadcast_workflow_lifecycle" in src, (
787
+ "import_workflow save path must broadcast via " "broadcast_workflow_lifecycle so connected clients refresh."
788
+ )
789
+ assert '"imported"' in src, (
790
+ "import_workflow must use the 'imported' lifecycle stage " "(matches WorkflowEvent.workflow_lifecycle Literal)."
791
+ )