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
@@ -3,7 +3,9 @@
3
3
  Locks in invariants 7 and 8 from docs-internal/credentials_panel.md:
4
4
  - store_api_key requires models=[]
5
5
  - Memory cache hit path does not query DB on every call
6
- - clear_cache wipes _memory_cache, _models_cache, _oauth_cache
6
+ - clear_cache wipes _api_key_cache, _oauth_cache (one merged cache
7
+ per RFC 9700 + post-Wave-12 dedup; previously the API-key half was
8
+ split across _memory_cache + _models_cache).
7
9
  """
8
10
 
9
11
  from __future__ import annotations
@@ -91,8 +93,7 @@ class TestMemoryCache:
91
93
  await auth_service.store_api_key("openai", "sk-foo", models=[])
92
94
 
93
95
  # Wipe memory cache to simulate fresh process
94
- auth_service._memory_cache.clear()
95
- auth_service._models_cache.clear()
96
+ auth_service._api_key_cache.clear()
96
97
 
97
98
  call_count = {"n": 0}
98
99
  original = auth_service.credentials_db.get_api_key
@@ -111,19 +112,17 @@ class TestMemoryCache:
111
112
  assert await auth_service.get_api_key("openai") == "sk-foo"
112
113
  assert call_count["n"] == 1
113
114
 
114
- async def test_clear_cache_wipes_all_three_caches(self, auth_service):
115
+ async def test_clear_cache_wipes_both_caches(self, auth_service):
115
116
  await auth_service.store_api_key("openai", "sk-foo", models=["gpt-4"])
116
117
  await auth_service.store_oauth_tokens("google", "a", "r", email="u@x.com")
117
118
 
118
119
  # Sanity: caches populated
119
- assert auth_service._memory_cache
120
- assert auth_service._models_cache
120
+ assert auth_service._api_key_cache
121
121
  assert auth_service._oauth_cache
122
122
 
123
123
  auth_service.clear_cache()
124
124
 
125
- assert auth_service._memory_cache == {}
126
- assert auth_service._models_cache == {}
125
+ assert auth_service._api_key_cache == {}
127
126
  assert auth_service._oauth_cache == {}
128
127
 
129
128
  async def test_clear_cache_does_not_delete_db_data(self, auth_service):
@@ -149,10 +148,18 @@ class TestOAuthOps:
149
148
  tokens = await auth_service.get_oauth_tokens("google")
150
149
  assert tokens is not None
151
150
  assert tokens["access_token"] == "access-1"
152
- assert tokens["refresh_token"] == "refresh-1"
151
+ # Per RFC 9700 the refresh token is NOT returned by
152
+ # get_oauth_tokens — read via get_oauth_refresh_token() instead.
153
+ assert "refresh_token" not in tokens
153
154
  assert tokens["email"] == "u@example.com"
154
155
  assert tokens["scopes"] == "openid email"
155
156
 
157
+ # The refresh token is reachable through the dedicated helper
158
+ # that always reads from the encrypted DB (no in-memory cache).
159
+ assert (
160
+ await auth_service.get_oauth_refresh_token("google") == "refresh-1"
161
+ )
162
+
156
163
  async def test_remove_oauth_tokens(self, auth_service):
157
164
  await auth_service.store_oauth_tokens("google", "a", "r")
158
165
  ok = await auth_service.remove_oauth_tokens("google")
@@ -0,0 +1,219 @@
1
+ """Pytest invariant: every credential-mutation handler MUST broadcast.
2
+
3
+ The frontend's `useCatalogueQuery` cache + `apiKeyStatuses` map both go
4
+ stale unless the backend explicitly invalidates via:
5
+
6
+ - ``broadcaster.update_api_key_status(...)`` — per-provider validation
7
+ state change (carries the validation result payload).
8
+ - ``broadcaster.broadcast_credential_event("credential.*", ...)`` —
9
+ CloudEvents-typed mutation (refetch signal). Wraps ``WorkflowEvent``
10
+ from ``services.events.envelope``.
11
+
12
+ Cross-tab visibility breaks the moment a handler omits both. This test
13
+ locks the contract by reading each handler's source via
14
+ ``inspect.getsource`` and asserting it contains at least one of the two
15
+ broadcast call patterns. New credential mutations that forget to
16
+ broadcast fail CI before they ship.
17
+
18
+ Companion: ``test_auth_service.py`` locks the DB-write-then-cache-update
19
+ ordering inside AuthService.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import inspect
25
+ import re
26
+
27
+ import pytest
28
+
29
+
30
+ pytestmark = pytest.mark.credentials
31
+
32
+
33
+ # Match either:
34
+ # broadcaster.update_api_key_status(...)
35
+ # broadcaster.broadcast_credential_event("credential.<anything>", ...)
36
+ # Anchor on `.update_api_key_status(` or `.broadcast_credential_event(`
37
+ # so renames force a test update (intentional).
38
+ _BROADCAST_PATTERN = re.compile(
39
+ r"\.(?:update_api_key_status|broadcast_credential_event)\s*\("
40
+ )
41
+
42
+
43
+ def _handler_source(handler) -> str:
44
+ """Return the unwrapped source of a handler decorated by `@ws_handler`."""
45
+ fn = handler
46
+ while hasattr(fn, "__wrapped__"):
47
+ fn = fn.__wrapped__
48
+ return inspect.getsource(fn)
49
+
50
+
51
+ class TestCredentialMutationHandlersBroadcast:
52
+ """Every handler that mutates credential state MUST broadcast.
53
+
54
+ Failing this test means a frontend cache will go stale across tabs
55
+ after the mutation; users will see the wrong stored / connected
56
+ state until they manually refresh.
57
+ """
58
+
59
+ def test_validate_api_key_broadcasts(self):
60
+ """Validation broadcasts come from ``Credential.validate`` now.
61
+
62
+ ``handle_validate_api_key`` is a pure dispatcher that calls
63
+ ``CREDENTIAL_REGISTRY[provider].validate(...)``. The broadcast
64
+ happens inside ``Credential.validate`` (the shared scaffold in
65
+ ``services/plugin/credential.py``). This test checks the
66
+ invariant at the point where it actually lives.
67
+ """
68
+ from services.plugin.credential import Credential
69
+
70
+ src = inspect.getsource(Credential.validate)
71
+ assert _BROADCAST_PATTERN.search(src), (
72
+ "Credential.validate must broadcast via update_api_key_status — "
73
+ "every credential mutation must surface to connected clients."
74
+ )
75
+
76
+ def test_save_api_key_broadcasts(self):
77
+ from routers import websocket as ws_module
78
+
79
+ src = _handler_source(ws_module.handle_save_api_key)
80
+ assert _BROADCAST_PATTERN.search(src), (
81
+ "handle_save_api_key must broadcast credential.api_key.saved "
82
+ "via broadcast_credential_event so the catalogue's stored "
83
+ "flag flips on every connected client"
84
+ )
85
+
86
+ def test_delete_api_key_broadcasts(self):
87
+ from routers import websocket as ws_module
88
+
89
+ src = _handler_source(ws_module.handle_delete_api_key)
90
+ # Delete must trigger BOTH broadcasts: api_key_status (clears
91
+ # apiKeyStatuses) + credential.api_key.deleted (catalogue refetch).
92
+ matches = _BROADCAST_PATTERN.findall(src)
93
+ assert len(matches) >= 2, (
94
+ "handle_delete_api_key must broadcast both update_api_key_status "
95
+ "(clears apiKeyStatuses[provider]) AND broadcast_credential_event "
96
+ "('credential.api_key.deleted') (catalogue refetch). "
97
+ f"Found {len(matches)} broadcast call(s)."
98
+ )
99
+
100
+ def test_twitter_logout_broadcasts(self):
101
+ # Twitter handler moved to ``nodes/twitter/_handlers.py`` as
102
+ # part of the plugin-extraction migration. The invariant now
103
+ # asserts on the plugin-owned source.
104
+ from nodes.twitter._handlers import handle_twitter_logout
105
+
106
+ src = _handler_source(handle_twitter_logout)
107
+ assert _BROADCAST_PATTERN.search(src), (
108
+ "handle_twitter_logout must broadcast credential.oauth.disconnected"
109
+ )
110
+
111
+ def test_google_logout_broadcasts(self):
112
+ from nodes.google._handlers import handle_google_logout
113
+
114
+ src = _handler_source(handle_google_logout)
115
+ assert _BROADCAST_PATTERN.search(src), (
116
+ "handle_google_logout must broadcast credential.oauth.disconnected"
117
+ )
118
+
119
+
120
+ class TestCredentialEventCloudEventsShape:
121
+ """The credential broadcast helper wraps `WorkflowEvent` so the
122
+ on-the-wire body is a CloudEvents v1.0 envelope. Locks the spec
123
+ fields so future refactors don't quietly drop required keys.
124
+ """
125
+
126
+ @pytest.fixture
127
+ def envelope(self):
128
+ from services.events.envelope import WorkflowEvent
129
+
130
+ event = WorkflowEvent(
131
+ source="machinaos://services/credentials",
132
+ type="credential.api_key.saved",
133
+ subject="openai",
134
+ data={"provider": "openai"},
135
+ )
136
+ return event.model_dump(mode="json")
137
+
138
+ def test_specversion_pinned_to_1(self, envelope):
139
+ assert envelope["specversion"] == "1.0"
140
+
141
+ def test_required_fields_present(self, envelope):
142
+ # CloudEvents 1.0 mandatory: id, source, specversion, type
143
+ for field in ("id", "source", "specversion", "type"):
144
+ assert envelope.get(field), f"missing required field: {field}"
145
+
146
+ def test_credential_subject_is_provider(self, envelope):
147
+ assert envelope["subject"] == "openai"
148
+
149
+ def test_event_type_namespaced(self, envelope):
150
+ # Convention: credential.<area>.<action>
151
+ assert envelope["type"].startswith("credential."), envelope["type"]
152
+
153
+
154
+ class TestAuthServiceDbCanonicalInvariant:
155
+ """AuthService.store_*/remove_* must do the DB call before touching
156
+ the in-memory cache. Reverse order leaves a stale-cache window.
157
+
158
+ Static-source check; doesn't run the methods.
159
+ """
160
+
161
+ @pytest.fixture
162
+ def auth_source(self):
163
+ from services import auth as auth_module
164
+
165
+ return inspect.getsource(auth_module.AuthService)
166
+
167
+ @pytest.mark.parametrize(
168
+ "method_name,db_call",
169
+ [
170
+ ("store_api_key", "credentials_db.save_api_key"),
171
+ ("remove_api_key", "credentials_db.delete_api_key"),
172
+ ("store_oauth_tokens", "credentials_db.save_oauth_tokens"),
173
+ ("remove_oauth_tokens", "credentials_db.delete_oauth_tokens"),
174
+ ],
175
+ )
176
+ def test_method_calls_db(self, auth_source, method_name, db_call):
177
+ """Every store_*/remove_* method must call the canonical DB
178
+ method. Invariant breaks if a refactor accidentally bypasses
179
+ the DB and only updates the cache."""
180
+ # Find the method body in the AuthService source. Anchor end on
181
+ # the next method/class start OR end-of-string (the last method
182
+ # in the class has no trailing sibling).
183
+ method_re = re.compile(
184
+ rf"async def {method_name}\b.*?(?=\n async def |\n def |\nclass |\Z)",
185
+ re.DOTALL,
186
+ )
187
+ match = method_re.search(auth_source)
188
+ assert match, f"AuthService.{method_name} not found in source"
189
+ body = match.group(0)
190
+ assert db_call in body, (
191
+ f"AuthService.{method_name} must call self.{db_call}() "
192
+ f"before touching the in-memory cache (DB is canonical "
193
+ f"source of truth)"
194
+ )
195
+
196
+ def test_oauth_cache_does_not_carry_refresh_token(self, auth_source):
197
+ """Per RFC 9700, refresh tokens must not live in process memory.
198
+ store_oauth_tokens caches only the access token + display fields.
199
+ """
200
+ match = re.search(
201
+ r"async def store_oauth_tokens\b.*?(?=\n async def |\n def |\nclass )",
202
+ auth_source,
203
+ re.DOTALL,
204
+ )
205
+ assert match, "AuthService.store_oauth_tokens not found"
206
+ body = match.group(0)
207
+
208
+ # Find the cache-write block. It assigns to self._oauth_cache[...].
209
+ cache_block = re.search(
210
+ r"self\._oauth_cache\[\w+\]\s*=\s*\{[^}]+\}", body, re.DOTALL
211
+ )
212
+ assert cache_block, (
213
+ "AuthService.store_oauth_tokens must populate _oauth_cache"
214
+ )
215
+ assert '"refresh_token"' not in cache_block.group(0), (
216
+ "RFC 9700 violation: _oauth_cache entry must not carry "
217
+ "refresh_token. Use get_oauth_refresh_token() helper that "
218
+ "always reads from the encrypted DB."
219
+ )
@@ -11,8 +11,8 @@ from urllib.parse import parse_qs, urlparse
11
11
 
12
12
  import pytest
13
13
 
14
- from services import google_oauth
15
- from services.google_oauth import GoogleOAuth, get_callback_paths
14
+ from nodes.google import _oauth as google_oauth
15
+ from nodes.google._oauth import GoogleOAuth, get_callback_paths
16
16
 
17
17
 
18
18
  pytestmark = pytest.mark.credentials
@@ -88,18 +88,18 @@ class TestAuthorizationUrl:
88
88
 
89
89
 
90
90
  class TestExchangeCode:
91
- def test_unknown_state_fails(self, oauth):
91
+ async def test_unknown_state_fails(self, oauth):
92
92
  # Don't even register a state -- exchange must fail without hitting Google.
93
- result = oauth.exchange_code("code", "never-generated-state")
93
+ result = await oauth.exchange_code("code", "never-generated-state")
94
94
  assert result["success"] is False
95
95
  assert "state" in result["error"].lower()
96
96
 
97
- def test_state_consumed_on_failure(self, oauth):
97
+ async def test_state_consumed_on_failure(self, oauth):
98
98
  """Even if the token exchange fails, the state must be popped (single use)."""
99
99
  auth = oauth.generate_authorization_url()
100
100
  state = auth["state"]
101
101
  # Exchange will fail because we haven't mocked Google -- network error
102
- oauth.exchange_code("invalid-code", state)
102
+ await oauth.exchange_code("invalid-code", state)
103
103
  # State must be gone regardless of success
104
104
  assert state not in google_oauth._oauth_states
105
105
 
@@ -80,7 +80,7 @@ class TestGetRedirectUri:
80
80
 
81
81
  def test_paths_loaded_from_config_not_hardcoded(self):
82
82
  """Smoke test: the real config must expose google + twitter paths."""
83
- from services.google_oauth import get_callback_paths
83
+ from nodes.google._oauth import get_callback_paths
84
84
 
85
85
  paths = get_callback_paths()
86
86
  assert "google" in paths
@@ -16,8 +16,8 @@ import httpx
16
16
  import pytest
17
17
  import respx
18
18
 
19
- from services import twitter_oauth
20
- from services.twitter_oauth import (
19
+ from nodes.twitter import _oauth as twitter_oauth
20
+ from nodes.twitter._oauth import (
21
21
  TwitterOAuth,
22
22
  TOKEN_URL,
23
23
  REVOKE_URL,
@@ -60,6 +60,10 @@ def patched_container(monkeypatch, auth_service):
60
60
  # not on services.status_broadcaster (the bound name in the handler module).
61
61
  fake_broadcaster = MagicMock()
62
62
  fake_broadcaster.update_api_key_status = AsyncMock()
63
+ # Wave-12 credential broadcast helper used by save / delete /
64
+ # oauth-logout handlers. Wraps WorkflowEvent (CloudEvents v1.0)
65
+ # and broadcasts as `credential_catalogue_updated`.
66
+ fake_broadcaster.broadcast_credential_event = AsyncMock()
63
67
  fake_broadcaster.broadcast = AsyncMock()
64
68
  fake_broadcaster._status = {}
65
69
  monkeypatch.setattr(ws_module, "get_status_broadcaster", lambda: fake_broadcaster)
@@ -110,22 +114,23 @@ class TestValidateApiKey:
110
114
  # Broadcaster notified with hasKey + models
111
115
  patched_container.broadcaster.update_api_key_status.assert_awaited_once()
112
116
 
113
- @patch("services.ai.PROVIDER_CONFIGS", {})
114
- @patch.object(ws_module, "_SPECIAL_PROVIDER_VALIDATORS", {})
115
- async def test_validate_skips_model_fetch_for_non_llm(
117
+ async def test_validate_unknown_provider_returns_error(
116
118
  self, patched_container, fake_ws
117
119
  ):
118
- # Pick a provider that's neither in PROVIDER_CONFIGS (LLM) nor in
119
- # _SPECIAL_PROVIDER_VALIDATORS (Google Maps / Apify probe). The
120
- # handler should fall through to the empty-models path.
120
+ # ``handle_validate_api_key`` dispatches via
121
+ # ``CREDENTIAL_REGISTRY``; a provider without a registered
122
+ # ``Credential`` subclass is an explicit error (no fall-through
123
+ # to the default LLM probe — every supported provider must own
124
+ # its credential class).
121
125
  result = await _call(
122
126
  ws_module.handle_validate_api_key,
123
127
  {"provider": "anonymous_provider", "api_key": "any-test"},
124
128
  fake_ws,
125
129
  )
126
130
 
127
- assert result["valid"] is True
128
- assert result["models"] == []
131
+ assert result["success"] is False
132
+ assert result["valid"] is False
133
+ assert "anonymous_provider" in result["error"]
129
134
  patched_container.ai.fetch_models.assert_not_called()
130
135
 
131
136
  async def test_missing_required_fields_returns_error(
@@ -214,14 +219,22 @@ class TestDeleteApiKey:
214
219
 
215
220
 
216
221
  class TestTwitterOAuthHandlers:
222
+ # Twitter handlers moved to ``nodes/twitter/_handlers.py`` as part
223
+ # of the plugin-extraction migration. The tests now import
224
+ # directly from the plugin folder; the dispatch contract via
225
+ # ``register_ws_handlers`` is exercised by
226
+ # ``test_plugin_self_containment.py``.
227
+
217
228
  async def test_login_fails_without_client_id(self, patched_container, fake_ws):
218
- result = await _call(ws_module.handle_twitter_oauth_login, {}, fake_ws)
229
+ from nodes.twitter._handlers import handle_twitter_oauth_login
230
+ result = await _call(handle_twitter_oauth_login, {}, fake_ws)
219
231
  assert result["success"] is False
220
232
  assert "Client ID" in result["error"]
221
233
 
222
234
  async def test_login_returns_authorization_url(
223
235
  self, patched_container, fake_ws
224
236
  ):
237
+ from nodes.twitter._handlers import handle_twitter_oauth_login
225
238
  await patched_container.auth.store_api_key(
226
239
  "twitter_client_id", "ci-test", models=[]
227
240
  )
@@ -229,41 +242,51 @@ class TestTwitterOAuthHandlers:
229
242
  "twitter_client_secret", "cs-test", models=[]
230
243
  )
231
244
 
232
- result = await _call(ws_module.handle_twitter_oauth_login, {}, fake_ws)
245
+ result = await _call(handle_twitter_oauth_login, {}, fake_ws)
233
246
 
234
247
  assert result["success"] is True
235
248
  assert result["url"].startswith("https://x.com/i/oauth2/authorize")
236
249
  assert "state" in result
237
250
 
238
251
  async def test_status_when_disconnected(self, patched_container, fake_ws):
239
- result = await _call(ws_module.handle_twitter_oauth_status, {}, fake_ws)
252
+ from nodes.twitter._handlers import handle_twitter_oauth_status
253
+ result = await _call(handle_twitter_oauth_status, {}, fake_ws)
240
254
  assert result["connected"] is False
241
255
  assert result["username"] is None
242
256
 
243
257
  async def test_logout_clears_oauth_tokens(self, patched_container, fake_ws):
244
- # Pre-populate OAuth tokens
258
+ from nodes.twitter._handlers import handle_twitter_logout
245
259
  await patched_container.auth.store_oauth_tokens(
246
260
  "twitter", "access", "refresh"
247
261
  )
248
- # Mock the revoke calls so we don't hit the network
249
- with patch("services.twitter_oauth.TwitterOAuth.revoke_token", new=AsyncMock(return_value={"success": True})):
250
- result = await _call(ws_module.handle_twitter_logout, {}, fake_ws)
262
+ with patch(
263
+ "nodes.twitter._oauth.TwitterOAuth.revoke_token",
264
+ new=AsyncMock(return_value={"success": True}),
265
+ ):
266
+ result = await _call(handle_twitter_logout, {}, fake_ws)
251
267
 
252
268
  assert result["success"] is True
253
269
  assert await patched_container.auth.get_oauth_tokens("twitter") is None
254
270
 
255
271
 
256
272
  class TestGoogleOAuthHandlers:
273
+ # Google OAuth handlers moved to ``nodes/google/_handlers.py`` as
274
+ # part of the plugin-extraction migration. Tests now import from
275
+ # the plugin folder; the dispatch contract is exercised by
276
+ # ``test_plugin_self_containment.py``.
277
+
257
278
  async def test_login_fails_without_client_credentials(
258
279
  self, patched_container, fake_ws
259
280
  ):
260
- result = await _call(ws_module.handle_google_oauth_login, {}, fake_ws)
281
+ from nodes.google._handlers import handle_google_oauth_login
282
+ result = await _call(handle_google_oauth_login, {}, fake_ws)
261
283
  assert result["success"] is False
262
284
  assert "Client ID" in result["error"]
263
285
 
264
286
  async def test_login_returns_authorization_url(
265
287
  self, patched_container, fake_ws
266
288
  ):
289
+ from nodes.google._handlers import handle_google_oauth_login
267
290
  await patched_container.auth.store_api_key(
268
291
  "google_client_id", "ci.apps.googleusercontent.com", models=[]
269
292
  )
@@ -271,7 +294,7 @@ class TestGoogleOAuthHandlers:
271
294
  "google_client_secret", "cs-test", models=[]
272
295
  )
273
296
 
274
- result = await _call(ws_module.handle_google_oauth_login, {}, fake_ws)
297
+ result = await _call(handle_google_oauth_login, {}, fake_ws)
275
298
 
276
299
  assert result["success"] is True
277
300
  assert result["url"].startswith("https://accounts.google.com/o/oauth2/auth")
@@ -279,15 +302,16 @@ class TestGoogleOAuthHandlers:
279
302
  assert "prompt=consent" in result["url"]
280
303
 
281
304
  async def test_status_when_disconnected(self, patched_container, fake_ws):
282
- result = await _call(ws_module.handle_google_oauth_status, {}, fake_ws)
283
- # success may be auto-injected by the decorator; check connected=False
305
+ from nodes.google._handlers import handle_google_oauth_status
306
+ result = await _call(handle_google_oauth_status, {}, fake_ws)
284
307
  assert result["connected"] is False
285
308
  assert result["email"] is None
286
309
 
287
310
  async def test_logout_removes_oauth_tokens(self, patched_container, fake_ws):
311
+ from nodes.google._handlers import handle_google_logout
288
312
  await patched_container.auth.store_oauth_tokens(
289
313
  "google", "access", "refresh", email="user@example.com"
290
314
  )
291
- result = await _call(ws_module.handle_google_logout, {}, fake_ws)
315
+ result = await _call(handle_google_logout, {}, fake_ws)
292
316
  assert result["success"] is True
293
317
  assert await patched_container.auth.get_oauth_tokens("google") is None
@@ -11,6 +11,7 @@ def test_native_providers_set():
11
11
  assert NATIVE_PROVIDERS == {
12
12
  "anthropic", "openai", "gemini", "openrouter", "xai",
13
13
  "deepseek", "kimi", "mistral",
14
+ "ollama", "lmstudio",
14
15
  }
15
16
 
16
17
 
@@ -107,7 +107,11 @@ async def test_fetch_models_uses_native_for_anthropic(ai_service):
107
107
  models = await ai_service.fetch_models("anthropic", "sk-ant-test")
108
108
 
109
109
  assert models == expected_models
110
- mock_factory.assert_called_once_with("anthropic", "sk-ant-test")
110
+ # ai.fetch_models forwards `proxy_url` (defaulting to None) to the native
111
+ # factory so the Ollama-pattern proxy override path stays uniform with
112
+ # execute_chat. Asserting the full call signature including the explicit
113
+ # `None` kwarg ensures the proxy path is not silently dropped.
114
+ mock_factory.assert_called_once_with("anthropic", "sk-ant-test", proxy_url=None)
111
115
 
112
116
 
113
117
  @pytest.mark.asyncio
@@ -91,33 +91,33 @@ async def _execute_current_time(
91
91
 
92
92
  async def _execute_duckduckgo_search(
93
93
  args: Dict[str, Any],
94
- node_params: Dict[str, Any] = None,
94
+ config: Dict[str, Any] = None,
95
95
  ) -> Dict[str, Any]:
96
- """Shim for deleted services.handlers.tools._execute_duckduckgo_search."""
97
- try:
98
- # Plugin layout under scaling branch: nodes/search/duckduckgo_search.py
99
- from nodes.search.duckduckgo_search import (
100
- DuckDuckGoSearchNode,
101
- DuckDuckGoSearchParams,
102
- )
103
- except ImportError:
104
- return {"error": "duckduckgo search plugin missing"}
96
+ """Flat (args, config) -> dict shim for the deleted
97
+ ``services.handlers.tools._execute_duckduckgo_search``.
105
98
 
106
- q = str(args.get("query", "")).strip()
107
- if not q:
99
+ Wave 11.I milestone O moved this here from production code -- it
100
+ only ever served contract tests that patch ``sys.modules['ddgs']``
101
+ and assert the flat output shape. Production callers go through
102
+ :class:`nodes.search.duckduckgo_search.DuckDuckGoSearchNode`.
103
+ """
104
+ config = config or {}
105
+ query = str(args.get("query", "")).strip()
106
+ if not query:
108
107
  return {"error": "No search query provided"}
109
-
110
- params = DuckDuckGoSearchParams(
111
- query=q,
112
- max_results=int(args.get("max_results", args.get("maxResults", 5))),
113
- )
114
-
115
- node = DuckDuckGoSearchNode()
116
- try:
117
- out = await node.search(_DummyContext(), params)
118
- except Exception as exc:
119
- return {"error": str(exc)}
120
- return out.model_dump() if hasattr(out, "model_dump") else dict(out)
108
+ max_results = int(config.get("max_results", args.get("max_results", 5)))
109
+ provider = config.get("provider", "duckduckgo")
110
+ from ddgs import DDGS
111
+ raw = list(DDGS().text(query, max_results=max_results))
112
+ results = [
113
+ {
114
+ "title": item.get("title", ""),
115
+ "snippet": item.get("body", ""),
116
+ "url": item.get("href", ""),
117
+ }
118
+ for item in raw
119
+ ]
120
+ return {"query": query, "provider": provider, "results": results}
121
121
 
122
122
 
123
123
  async def handle_write_todos(