machinaos 0.0.76 → 0.0.78

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 (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
@@ -0,0 +1,188 @@
1
+ """Coverage for ``services.memory.jsonl`` — parse / append / trim.
2
+
3
+ Anthropic Messages API JSONL is the storage format the
4
+ ``claude_code_agent`` bridge round-trips. These tests pin the public
5
+ contract: standard parsers ignore unknown metadata, append always
6
+ emits a trailing newline, and trim returns removed lines verbatim for
7
+ vector-store archival.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+
14
+ from langchain_core.messages import AIMessage, HumanMessage
15
+
16
+ from services.memory.jsonl import append_message, parse_jsonl, trim_window
17
+ from services.memory import ( # public re-exports
18
+ parse_jsonl as parse_jsonl_reexport,
19
+ append_message as append_message_reexport,
20
+ trim_window as trim_window_reexport,
21
+ )
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # parse_jsonl
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ def test_parse_jsonl_empty_input_returns_empty_list():
30
+ assert parse_jsonl("") == []
31
+ assert parse_jsonl(None) == [] # type: ignore[arg-type]
32
+
33
+
34
+ def test_parse_jsonl_basic_user_assistant_pair():
35
+ text = (
36
+ '{"role": "user", "content": "hi"}\n'
37
+ '{"role": "assistant", "content": "hello"}\n'
38
+ )
39
+ msgs = parse_jsonl(text)
40
+ assert len(msgs) == 2
41
+ assert isinstance(msgs[0], HumanMessage) and msgs[0].content == "hi"
42
+ assert isinstance(msgs[1], AIMessage) and msgs[1].content == "hello"
43
+
44
+
45
+ def test_parse_jsonl_skips_unparseable_lines_forward_compat():
46
+ text = (
47
+ '{"role": "user", "content": "ok"}\n'
48
+ "this is not json\n"
49
+ '{"role": "assistant", "content": "still ok"}\n'
50
+ )
51
+ msgs = parse_jsonl(text)
52
+ assert [m.content for m in msgs] == ["ok", "still ok"]
53
+
54
+
55
+ def test_parse_jsonl_skips_unknown_roles():
56
+ text = (
57
+ '{"role": "system", "content": "unknown role"}\n'
58
+ '{"role": "tool_use", "content": "skipped"}\n'
59
+ '{"role": "user", "content": "kept"}\n'
60
+ )
61
+ msgs = parse_jsonl(text)
62
+ assert [m.content for m in msgs] == ["kept"]
63
+
64
+
65
+ def test_parse_jsonl_collapses_content_blocks_to_text():
66
+ text = json.dumps({
67
+ "role": "assistant",
68
+ "content": [
69
+ {"type": "text", "text": "Got it"},
70
+ {"type": "tool_use", "id": "tu_1", "name": "search", "input": {}},
71
+ {"type": "text", "text": "blue."},
72
+ ],
73
+ }) + "\n"
74
+ msgs = parse_jsonl(text)
75
+ assert len(msgs) == 1
76
+ assert msgs[0].content == "Got it blue." # text blocks joined; tool_use dropped
77
+
78
+
79
+ def test_parse_jsonl_metadata_keys_are_ignored():
80
+ text = json.dumps({
81
+ "role": "user",
82
+ "content": "hi",
83
+ "timestamp": "2026-05-10T00:00:00Z",
84
+ "session_id": "abc-123",
85
+ "model": "claude",
86
+ }) + "\n"
87
+ msgs = parse_jsonl(text)
88
+ assert len(msgs) == 1 and msgs[0].content == "hi"
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # append_message
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ def test_append_message_to_empty_string_yields_single_line_with_newline():
97
+ out = append_message("", "user", "hi")
98
+ assert out == '{"role": "user", "content": "hi"}\n'
99
+
100
+
101
+ def test_append_message_chains_cleanly_across_calls():
102
+ text = ""
103
+ text = append_message(text, "user", "q1")
104
+ text = append_message(text, "assistant", "a1")
105
+ lines = [ln for ln in text.splitlines() if ln.strip()]
106
+ assert len(lines) == 2
107
+ assert json.loads(lines[0])["role"] == "user"
108
+ assert json.loads(lines[1])["role"] == "assistant"
109
+
110
+
111
+ def test_append_message_normalises_missing_trailing_newline():
112
+ base = '{"role": "user", "content": "hi"}' # no trailing newline
113
+ out = append_message(base, "assistant", "ok")
114
+ assert out.startswith('{"role": "user", "content": "hi"}\n')
115
+ assert out.endswith('"content": "ok"}\n')
116
+
117
+
118
+ def test_append_message_metadata_round_trips_via_parse_jsonl():
119
+ text = append_message(
120
+ "", "assistant", "blue.",
121
+ timestamp="2026-05-10T12:34:56+00:00",
122
+ session_id="abc-123",
123
+ model="claude",
124
+ )
125
+ obj = json.loads(text.strip())
126
+ assert obj["timestamp"] == "2026-05-10T12:34:56+00:00"
127
+ assert obj["session_id"] == "abc-123"
128
+ assert obj["model"] == "claude"
129
+ # parse_jsonl still returns the message; metadata is preserved on the
130
+ # wire even if it's not surfaced on BaseMessage.
131
+ msgs = parse_jsonl(text)
132
+ assert msgs[0].content == "blue."
133
+
134
+
135
+ def test_append_message_supports_non_ascii_content():
136
+ out = append_message("", "user", "héllo — 你好")
137
+ obj = json.loads(out.strip())
138
+ assert obj["content"] == "héllo — 你好" # ensure_ascii=False preserves UTF-8
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # trim_window
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def test_trim_window_under_capacity_returns_input_unchanged_and_no_removed():
147
+ text = ""
148
+ text = append_message(text, "user", "q1")
149
+ text = append_message(text, "assistant", "a1")
150
+ trimmed, removed = trim_window(text, window_size=2)
151
+ assert trimmed == text
152
+ assert removed == []
153
+
154
+
155
+ def test_trim_window_removes_oldest_pairs_first():
156
+ text = ""
157
+ text = append_message(text, "user", "q1")
158
+ text = append_message(text, "assistant", "a1")
159
+ text = append_message(text, "user", "q2")
160
+ text = append_message(text, "assistant", "a2")
161
+ text = append_message(text, "user", "q3")
162
+ text = append_message(text, "assistant", "a3")
163
+ trimmed, removed = trim_window(text, window_size=1)
164
+ # window=1 keeps last 2 lines; removes 4 oldest.
165
+ assert len(removed) == 4
166
+ kept = [json.loads(ln) for ln in trimmed.splitlines() if ln.strip()]
167
+ assert [k["content"] for k in kept] == ["q3", "a3"]
168
+ # Removed entries are returned verbatim so the vector store gets
169
+ # the raw JSONL line back.
170
+ removed_objs = [json.loads(ln) for ln in removed]
171
+ assert [o["content"] for o in removed_objs] == ["q1", "a1", "q2", "a2"]
172
+
173
+
174
+ def test_trim_window_handles_empty_text():
175
+ trimmed, removed = trim_window("", 5)
176
+ assert trimmed == "" and removed == []
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Public-API re-exports through services.memory
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ def test_services_memory_reexports_match_jsonl_module():
185
+ """`services.memory.__init__` re-exports the JSONL public surface."""
186
+ assert parse_jsonl_reexport is parse_jsonl
187
+ assert append_message_reexport is append_message
188
+ assert trim_window_reexport is trim_window
@@ -0,0 +1,333 @@
1
+ """Contract tests for the generalized event-source framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import time
11
+ from typing import Iterable
12
+ from unittest.mock import patch
13
+
14
+ import pytest
15
+
16
+ pytestmark = pytest.mark.node_contract
17
+
18
+
19
+ def _run(coro):
20
+ return asyncio.new_event_loop().run_until_complete(coro)
21
+
22
+
23
+ # ============================================================================
24
+ # WorkflowEvent envelope
25
+ # ============================================================================
26
+
27
+
28
+ class TestWorkflowEvent:
29
+ def test_required_fields(self):
30
+ from services.events import WorkflowEvent
31
+ ev = WorkflowEvent(source="stripe://acct_1", type="stripe.charge.succeeded")
32
+ assert ev.specversion == "1.0"
33
+ assert ev.id # uuid default
34
+ assert ev.datacontenttype == "application/json"
35
+
36
+ def test_round_trip_json(self):
37
+ from services.events import WorkflowEvent
38
+ ev = WorkflowEvent(
39
+ source="stripe://acct_1", type="stripe.charge.succeeded", data={"amount": 1000},
40
+ )
41
+ restored = WorkflowEvent.model_validate(json.loads(ev.model_dump_json()))
42
+ assert restored.type == ev.type
43
+ assert restored.data == {"amount": 1000}
44
+ assert restored.id == ev.id
45
+
46
+ def test_matches_type_glob(self):
47
+ from services.events import WorkflowEvent
48
+ ev = WorkflowEvent(source="stripe://x", type="stripe.charge.succeeded")
49
+ assert ev.matches_type("all") is True
50
+ assert ev.matches_type("") is True
51
+ assert ev.matches_type("stripe.charge.succeeded") is True
52
+ assert ev.matches_type("stripe.charge.*") is True
53
+ assert ev.matches_type("stripe.*") is True
54
+ assert ev.matches_type("payment_intent.*") is False
55
+
56
+ def test_from_legacy(self):
57
+ from services.events import WorkflowEvent
58
+ ev = WorkflowEvent.from_legacy("whatsapp_message_received", {"text": "hi"})
59
+ assert ev.type == "whatsapp_message_received"
60
+ assert ev.data == {"text": "hi"}
61
+ assert ev.source.startswith("legacy://")
62
+
63
+
64
+ # ============================================================================
65
+ # Verifiers
66
+ # ============================================================================
67
+
68
+
69
+ class TestStripeVerifier:
70
+ SECRET = "whsec_test"
71
+
72
+ def _sign(self, body: bytes, ts: int | None = None) -> dict:
73
+ ts = ts if ts is not None else int(time.time())
74
+ signed = f"{ts}.".encode() + body
75
+ sig = hmac.new(self.SECRET.encode(), signed, hashlib.sha256).hexdigest()
76
+ return {"Stripe-Signature": f"t={ts},v1={sig}"}
77
+
78
+ def test_valid_signature_passes(self):
79
+ from services.events import StripeVerifier
80
+ body = b'{"id":"evt_1"}'
81
+ StripeVerifier.verify(self._sign(body), body, self.SECRET)
82
+
83
+ def test_tampered_body_rejected(self):
84
+ from services.events import StripeVerifier
85
+ body = b'{"id":"evt_1"}'
86
+ headers = self._sign(body)
87
+ with pytest.raises(ValueError):
88
+ StripeVerifier.verify(headers, b'{"id":"evt_2"}', self.SECRET)
89
+
90
+ def test_missing_header_rejected(self):
91
+ from services.events import StripeVerifier
92
+ with pytest.raises(ValueError, match="missing"):
93
+ StripeVerifier.verify({}, b'{}', self.SECRET)
94
+
95
+ def test_malformed_header_rejected(self):
96
+ from services.events import StripeVerifier
97
+ with pytest.raises(ValueError):
98
+ StripeVerifier.verify({"Stripe-Signature": "garbage"}, b'{}', self.SECRET)
99
+
100
+
101
+ class TestGitHubVerifier:
102
+ def test_round_trip(self):
103
+ from services.events import GitHubVerifier
104
+ secret = "shh"
105
+ body = b'{"action":"opened"}'
106
+ sig = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
107
+ GitHubVerifier.verify({"X-Hub-Signature-256": sig}, body, secret)
108
+
109
+ def test_tampered_rejected(self):
110
+ from services.events import GitHubVerifier
111
+ with pytest.raises(ValueError):
112
+ GitHubVerifier.verify({"X-Hub-Signature-256": "sha256=deadbeef"}, b'{}', "shh")
113
+
114
+
115
+ class TestStandardWebhooksVerifier:
116
+ def test_round_trip(self):
117
+ from services.events import StandardWebhooksVerifier
118
+ secret_raw = b"super-secret-key-bytes"
119
+ secret = "whsec_" + base64.b64encode(secret_raw).decode()
120
+ body = b'{"foo":"bar"}'
121
+ msg_id = "msg_abc"
122
+ ts = "1700000000"
123
+ signed = f"{msg_id}.{ts}.".encode() + body
124
+ sig = base64.b64encode(hmac.new(secret_raw, signed, hashlib.sha256).digest()).decode()
125
+ headers = {
126
+ "webhook-id": msg_id,
127
+ "webhook-timestamp": ts,
128
+ "webhook-signature": f"v1,{sig}",
129
+ }
130
+ StandardWebhooksVerifier.verify(headers, body, secret)
131
+
132
+ def test_tampered_rejected(self):
133
+ from services.events import StandardWebhooksVerifier
134
+ with pytest.raises(ValueError):
135
+ StandardWebhooksVerifier.verify(
136
+ {"webhook-id": "1", "webhook-timestamp": "1", "webhook-signature": "v1,bad"},
137
+ b'{}', "whsec_AAAA",
138
+ )
139
+
140
+
141
+ class TestHmacVerifier:
142
+ def test_round_trip(self):
143
+ from services.events import HmacVerifier
144
+ secret = "shh"
145
+ body = b'{"x":1}'
146
+ sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
147
+ HmacVerifier.verify({"X-Signature-256": sig}, body, secret)
148
+
149
+
150
+ # ============================================================================
151
+ # PollingEventSource loop
152
+ # ============================================================================
153
+
154
+
155
+ class TestPollingEventSource:
156
+ def test_emits_what_poll_once_returns_then_sleeps(self):
157
+ from services.events import PollingEventSource, WorkflowEvent
158
+
159
+ class FakePolling(PollingEventSource):
160
+ type = "fake.polling"
161
+ poll_interval_default = 0 # tight loop for the test
162
+
163
+ def __init__(self):
164
+ super().__init__()
165
+ self.calls = 0
166
+
167
+ async def poll_once(self, state) -> Iterable[WorkflowEvent]:
168
+ self.calls += 1
169
+ if self.calls == 1:
170
+ return [WorkflowEvent(source="x", type="fake.tick", data={"n": 1})]
171
+ self._stopped = True
172
+ return []
173
+
174
+ async def drain():
175
+ src = FakePolling()
176
+ seen = []
177
+ async for ev in src.emit():
178
+ seen.append(ev)
179
+ if len(seen) >= 1:
180
+ src._stopped = True
181
+ return seen
182
+
183
+ events = _run(drain())
184
+ assert len(events) == 1
185
+ assert events[0].type == "fake.tick"
186
+
187
+
188
+ # ============================================================================
189
+ # WebhookSource — handle() integration
190
+ # ============================================================================
191
+
192
+
193
+ class TestWebhookSourceHandle:
194
+ def _build_source(self, secret_value: str | None):
195
+ from services.events import StripeVerifier, WebhookSource, WorkflowEvent
196
+
197
+ class _Cred:
198
+ @classmethod
199
+ async def resolve(cls):
200
+ if secret_value is None:
201
+ raise PermissionError
202
+ return {"api_key": "sk_test", "test_secret": secret_value}
203
+
204
+ class FakeSource(WebhookSource):
205
+ type = "fake.hook"
206
+ path = "fake"
207
+ verifier = StripeVerifier
208
+ secret_field = "test_secret"
209
+ credential = _Cred
210
+
211
+ async def shape(self, request, body, payload):
212
+ return WorkflowEvent(source="fake://x", type="fake.event", data=payload)
213
+
214
+ return FakeSource()
215
+
216
+ def _signed_request(self, body: bytes, secret: str):
217
+ ts = int(time.time())
218
+ signed = f"{ts}.".encode() + body
219
+ sig = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
220
+
221
+ class FakeReq:
222
+ headers = {"Stripe-Signature": f"t={ts},v1={sig}"}
223
+
224
+ async def body(self):
225
+ return body
226
+
227
+ return FakeReq()
228
+
229
+ def test_valid_signature_dispatches(self):
230
+ src = self._build_source(secret_value="whsec_test")
231
+ body = b'{"event":"payload"}'
232
+ req = self._signed_request(body, "whsec_test")
233
+ with patch("services.event_waiter.dispatch") as dispatch:
234
+ ev = _run(src.handle(req))
235
+ assert ev.type == "fake.event"
236
+ dispatch.assert_called_once()
237
+ called_type, called_event = dispatch.call_args[0]
238
+ assert called_type == "fake.hook"
239
+ assert called_event is ev
240
+
241
+ def test_tampered_signature_raises_400(self):
242
+ from fastapi import HTTPException
243
+ src = self._build_source(secret_value="whsec_test")
244
+ body = b'{"event":"payload"}'
245
+ req = self._signed_request(body, "whsec_other") # signed with different secret
246
+ with pytest.raises(HTTPException) as exc:
247
+ _run(src.handle(req))
248
+ assert exc.value.status_code == 400
249
+
250
+ def test_missing_secret_accepts_unverified(self):
251
+ """No secret captured yet -> log warning, accept event."""
252
+ src = self._build_source(secret_value=None)
253
+ body = b'{"event":"payload"}'
254
+
255
+ class FakeReq:
256
+ headers = {}
257
+
258
+ async def body(self):
259
+ return body
260
+
261
+ with patch("services.event_waiter.dispatch") as dispatch:
262
+ ev = _run(src.handle(FakeReq()))
263
+ assert ev.type == "fake.event"
264
+ dispatch.assert_called_once()
265
+
266
+
267
+ # ============================================================================
268
+ # DaemonEventSource lifecycle (mocked ProcessService)
269
+ # ============================================================================
270
+
271
+
272
+ class TestDaemonEventSource:
273
+ def test_start_calls_process_service_start(self):
274
+ from services.events import DaemonEventSource
275
+
276
+ class FakeDaemon(DaemonEventSource):
277
+ type = "fake.daemon"
278
+ process_name = "fake-daemon"
279
+ binary_name = "echo" # always on PATH
280
+
281
+ def build_command(self, secrets):
282
+ return "echo hello"
283
+
284
+ def parse_line(self, stream, line):
285
+ return None
286
+
287
+ async def go():
288
+ with patch("services.events.daemon.shutil.which", return_value="/usr/bin/echo"), \
289
+ patch("services.events.daemon.get_process_service") as get_ps:
290
+ ps = get_ps.return_value
291
+
292
+ async def fake_start(**kwargs):
293
+ return {"success": True, "result": {"pid": 4242}}
294
+
295
+ ps.start.side_effect = fake_start
296
+
297
+ async def fake_stop(**kwargs):
298
+ return {"success": True}
299
+
300
+ ps.stop.side_effect = fake_stop
301
+
302
+ src = FakeDaemon()
303
+ result = await src.start()
304
+ assert result["success"] is True
305
+ assert src.pid == 4242
306
+ ps.start.assert_called_once()
307
+ # Cancel the tail task so the test exits cleanly.
308
+ await src.stop()
309
+ assert src.pid is None
310
+
311
+ _run(go())
312
+
313
+ def test_start_fails_when_binary_missing(self):
314
+ from services.events import DaemonEventSource
315
+
316
+ class FakeDaemon(DaemonEventSource):
317
+ type = "fake.daemon"
318
+ process_name = "fake-daemon"
319
+ binary_name = "definitely-not-on-path-zzzzz"
320
+ install_hint = "see docs"
321
+
322
+ def build_command(self, secrets):
323
+ return "x"
324
+
325
+ async def go():
326
+ with patch("services.events.daemon.shutil.which", return_value=None):
327
+ src = FakeDaemon()
328
+ result = await src.start()
329
+ assert result["success"] is False
330
+ assert "PATH" in result["error"]
331
+ assert "see docs" in result["error"]
332
+
333
+ _run(go())
@@ -1,9 +1,10 @@
1
1
  """Wave 6 NodeSpec contract tests.
2
2
 
3
3
  Locks in the public shape emitted by services/node_input_schemas.py,
4
- services/node_spec.py, services/node_option_loaders, and the
5
- /api/schemas/nodes/*/spec.json + /api/schemas/nodes/options/* endpoints.
6
- Mirrors the Wave 3 test posture for node_output_schemas.
4
+ services/node_spec.py, services/ws_handler_registry (option-loader
5
+ registry, post-Wave-11.I M.3), and the /api/schemas/nodes/*/spec.json +
6
+ /api/schemas/nodes/options/* endpoints. Mirrors the Wave 3 test posture
7
+ for node_output_schemas.
7
8
  """
8
9
 
9
10
  import pytest # noqa: F401 (used by @pytest.mark.asyncio on Phase 4 tests)
@@ -16,6 +17,19 @@ from services.node_input_schemas import (
16
17
  from services.node_spec import get_node_spec, list_node_types_with_spec
17
18
 
18
19
 
20
+ def _numeric_constraints(prop: dict) -> dict:
21
+ """Pydantic emits ``Optional[float] = Field(ge=..., le=...)`` as
22
+ ``{anyOf: [{number, minimum, maximum}, {null}]}``. This helper digs
23
+ out the numeric branch so tests don't have to special-case nullable
24
+ vs. required fields each time we tweak Params."""
25
+ if "minimum" in prop or "maximum" in prop:
26
+ return prop
27
+ for branch in prop.get("anyOf", ()):
28
+ if branch.get("type") == "number" or "minimum" in branch or "maximum" in branch:
29
+ return branch
30
+ return prop
31
+
32
+
19
33
  class TestInputSchemas:
20
34
  def test_registry_not_empty(self):
21
35
  assert len(NODE_INPUT_MODELS) > 50
@@ -35,8 +49,9 @@ class TestInputSchemas:
35
49
  schema = get_node_input_schema("aiAgent")
36
50
  assert schema is not None
37
51
  props = schema["properties"]
38
- assert props["temperature"]["minimum"] == 0.0
39
- assert props["temperature"]["maximum"] == 2.0
52
+ temp = _numeric_constraints(props["temperature"])
53
+ assert temp["minimum"] == 0.0
54
+ assert temp["maximum"] == 2.0
40
55
  # snake_case keys per Wave 11.E.4+ convention.
41
56
  # NB: ``api_key`` is NOT a declared field — credentials live in
42
57
  # the credentials DB and are auto-injected at execution time.
@@ -237,7 +252,7 @@ class TestPhase3cCoverage:
237
252
  # All 16 specialized agents share SpecializedAgentParams - constraints
238
253
  # like temperature 0-2 should appear identically.
239
254
  spec = get_node_spec("coding_agent")
240
- temp = spec["inputs"]["properties"]["temperature"]
255
+ temp = _numeric_constraints(spec["inputs"]["properties"]["temperature"])
241
256
  assert temp["minimum"] == 0.0
242
257
  assert temp["maximum"] == 2.0
243
258
 
@@ -372,31 +387,33 @@ class TestPhase4LoadOptions:
372
387
  """Wave 6 Phase 4: unified loadOptionsMethod dispatch registry."""
373
388
 
374
389
  def test_registry_has_whatsapp_methods(self):
375
- from services.node_option_loaders import LOAD_OPTIONS_REGISTRY
390
+ from services.ws_handler_registry import list_load_options_methods
391
+ methods = list_load_options_methods()
376
392
  for method in ["whatsappGroups", "whatsappChannels", "whatsappGroupMembers"]:
377
- assert method in LOAD_OPTIONS_REGISTRY
393
+ assert method in methods
378
394
 
379
395
  def test_registry_has_google_methods(self):
380
- from services.node_option_loaders import LOAD_OPTIONS_REGISTRY
396
+ from services.ws_handler_registry import list_load_options_methods
397
+ methods = list_load_options_methods()
381
398
  for method in ["gmailLabels", "googleCalendarList", "googleDriveFolders", "googleTasklists"]:
382
- assert method in LOAD_OPTIONS_REGISTRY
399
+ assert method in methods
383
400
 
384
401
  def test_list_methods_sorted(self):
385
- from services.node_option_loaders import list_load_options_methods
402
+ from services.ws_handler_registry import list_load_options_methods
386
403
  methods = list_load_options_methods()
387
404
  assert methods == sorted(methods)
388
405
  assert len(methods) >= 7
389
406
 
390
407
  @pytest.mark.asyncio
391
408
  async def test_unknown_method_returns_empty(self):
392
- from services.node_option_loaders import dispatch_load_options
409
+ from services.ws_handler_registry import dispatch_load_options
393
410
  result = await dispatch_load_options("nonExistentMethodXyz", {})
394
411
  assert result == []
395
412
 
396
413
  @pytest.mark.asyncio
397
414
  async def test_dispatch_passes_params(self):
398
415
  # Smoke test: unknown method tolerates arbitrary params, doesn't crash
399
- from services.node_option_loaders import dispatch_load_options
416
+ from services.ws_handler_registry import dispatch_load_options
400
417
  result = await dispatch_load_options("unknown", {"group_id": "abc"})
401
418
  assert result == []
402
419
 
@@ -730,17 +747,19 @@ class TestNodeSpecContractInvariants:
730
747
 
731
748
  def test_load_options_methods_are_registered(self):
732
749
  """Every Pydantic Field(loadOptionsMethod=X) must point at a
733
- method registered in LOAD_OPTIONS_REGISTRY. Catches typos and
750
+ registered loader -- either in the legacy table or in a
751
+ plugin's ``register_option_loader`` call. Catches typos and
734
752
  forgotten loader registrations."""
735
753
  from services.node_input_schemas import get_node_input_schema, NODE_INPUT_MODELS
736
- from services.node_option_loaders import LOAD_OPTIONS_REGISTRY
754
+ from services.ws_handler_registry import list_load_options_methods
755
+ methods = set(list_load_options_methods())
737
756
  for t in NODE_INPUT_MODELS:
738
757
  schema = get_node_input_schema(t)
739
758
  for prop_name, prop in schema.get("properties", {}).items():
740
759
  method = prop.get("loadOptionsMethod")
741
760
  if method is None:
742
761
  continue
743
- assert method in LOAD_OPTIONS_REGISTRY, (
762
+ assert method in methods, (
744
763
  f"{t}.{prop_name} references unknown loadOptionsMethod {method!r}"
745
764
  )
746
765
 
@@ -777,6 +796,11 @@ class TestNodeSpecContractInvariants:
777
796
  "width", "height",
778
797
  # Wave 10.G.5: start-node's user-authored JSON blob marker
779
798
  "hasInitialDataBlob",
799
+ # Auto-derived from group membership ('memory' / 'tool') by
800
+ # _derive_auto_ui_hints in services/plugin/base.py. Tells the
801
+ # parameter panel that this node is an auxiliary config node
802
+ # and should inherit the parent's main inputs.
803
+ "isConfigNode",
780
804
  }
781
805
  for node_type, meta in NODE_METADATA.items():
782
806
  hints = meta.get("uiHints") or {}
@@ -882,10 +906,26 @@ class TestPluginContractInvariants:
882
906
  def test_hide_output_handle_nodes_really_have_no_output(self):
883
907
  # If a node advertises hideOutputHandle=True, it shouldn't also
884
908
  # declare an output in its handles list — inconsistent.
909
+ # Exception: tool-oriented nodes (component_kind="tool" pure
910
+ # ToolNodes, or ActionNode + usable_as_tool=True dual-use nodes)
911
+ # have hide_output_handle auto-derived True via
912
+ # BaseNode.__init_subclass__. Their declared handles tuple still
913
+ # carries output-tool / output-main for backend awareness, but
914
+ # the SquareNode frontend suppresses the default render. The
915
+ # invariant's original intent was "explicit author intent should
916
+ # be consistent" — auto-derived is not author intent, so skip
917
+ # those nodes.
918
+ from services.node_registry import get_node_class
885
919
  for t in self._plugin_types():
886
920
  spec = get_node_spec(t)
887
921
  if not spec.get("hideOutputHandle"):
888
922
  continue
923
+ cls = get_node_class(t)
924
+ if cls is not None and (
925
+ getattr(cls, "usable_as_tool", False)
926
+ or getattr(cls, "component_kind", "") == "tool"
927
+ ):
928
+ continue
889
929
  outs = [h for h in spec.get("handles") or [] if h.get("kind") == "output"]
890
930
  assert not outs, (
891
931
  f"{t}: hideOutputHandle=True but handles declares output(s) {outs}"