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,439 @@
1
+ """Contract tests for the agentBuilder multi-op plugin.
2
+
3
+ agentBuilder is a single multi-op tool node exposing 5 canvas-mutation
4
+ operations through the standard `@Operation` plugin pattern (matching
5
+ gmail / calendar / drive). The LLM sees one tool with an `operation`
6
+ discriminator; we test each operation's happy + reject paths directly
7
+ on the node instance, mocking the broadcaster and registry lookups so
8
+ no live state is touched.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from types import SimpleNamespace
14
+ from unittest.mock import AsyncMock, MagicMock, patch
15
+
16
+ import pytest
17
+
18
+ from nodes.tool import agent_builder as ab
19
+ from services.plugin import NodeContext
20
+
21
+
22
+ pytestmark = pytest.mark.node_contract
23
+
24
+
25
+ # ============================================================================
26
+ # Helpers
27
+ # ============================================================================
28
+
29
+
30
+ def _make_ctx(
31
+ *,
32
+ nodes=None,
33
+ edges=None,
34
+ workflow_id: str = "wf-test",
35
+ self_id: str = "ab-1",
36
+ ) -> NodeContext:
37
+ return NodeContext(
38
+ node_id=self_id,
39
+ node_type="agentBuilder",
40
+ workflow_id=workflow_id,
41
+ nodes=nodes or [],
42
+ edges=edges or [],
43
+ raw={"workflow_id": workflow_id},
44
+ )
45
+
46
+
47
+ def _agent_node(node_id: str, node_type: str = "aiAgent", **params) -> dict:
48
+ return {
49
+ "id": node_id,
50
+ "type": node_type,
51
+ "data": {"label": node_type, "parameters": params},
52
+ }
53
+
54
+
55
+ def _edge(source: str, target: str, target_handle: str = "input-tools") -> dict:
56
+ return {
57
+ "source": source,
58
+ "target": target,
59
+ "sourceHandle": "output-main",
60
+ "targetHandle": target_handle,
61
+ }
62
+
63
+
64
+ def _node_class(component_kind: str) -> SimpleNamespace:
65
+ return SimpleNamespace(component_kind=component_kind)
66
+
67
+
68
+ def _registry(**type_to_kind) -> dict:
69
+ return {ntype: _node_class(kind) for ntype, kind in type_to_kind.items()}
70
+
71
+
72
+ # ============================================================================
73
+ # Plugin registration
74
+ # ============================================================================
75
+
76
+
77
+ class TestRegistration:
78
+ def test_node_is_registered(self):
79
+ from services.node_registry import get_node_class
80
+ cls = get_node_class("agentBuilder")
81
+ assert cls is ab.AgentBuilderNode
82
+
83
+ def test_has_five_operations(self):
84
+ ops = set(ab.AgentBuilderNode._operations.keys())
85
+ assert ops == {
86
+ "inspect_canvas", "add_tool", "add_skill",
87
+ "add_subagent", "create_workflow",
88
+ }
89
+
90
+ def test_default_operation_is_inspect_canvas(self):
91
+ params = ab.AgentBuilderParams()
92
+ assert params.operation == "inspect_canvas"
93
+
94
+ def test_is_tool_node(self):
95
+ """agentBuilder is a tool-only plugin; subclasses ToolNode (not
96
+ ActionNode + usable_as_tool=True). The ToolNode base IS the
97
+ contract for nodes that exist solely to be invoked by an LLM
98
+ through an agent's input-tools handle."""
99
+ from services.plugin import ToolNode
100
+ assert issubclass(ab.AgentBuilderNode, ToolNode)
101
+
102
+ def test_handle_topology_matches_canonical_tool_shape(self):
103
+ """Canonical tool node: input-main left + output-tool top role=tools.
104
+ The output-tool shape is what wires into an agent's input-tools handle."""
105
+ handles = list(ab.AgentBuilderNode.handles)
106
+ assert len(handles) == 2
107
+ names = {h["name"] for h in handles}
108
+ assert names == {"input-main", "output-tool"}
109
+ out = next(h for h in handles if h["name"] == "output-tool")
110
+ assert out["kind"] == "output"
111
+ assert out["position"] == "top"
112
+ assert out["role"] == "tools"
113
+
114
+ def test_no_provided_tools_dict(self):
115
+ """Sanity check: the tribal PROVIDED_TOOLS attr from the original
116
+ design must NOT exist on the multi-op rewrite."""
117
+ assert not hasattr(ab.AgentBuilderNode, "PROVIDED_TOOLS")
118
+
119
+
120
+ # ============================================================================
121
+ # inspect_canvas
122
+ # ============================================================================
123
+
124
+
125
+ class TestInspectCanvas:
126
+ async def test_returns_node_and_edge_summaries(self):
127
+ nodes = [
128
+ _agent_node("agent-1"),
129
+ {"id": "ab-1", "type": "agentBuilder", "data": {"label": "AB"}},
130
+ {"id": "tool-1", "type": "httpRequest",
131
+ "data": {"label": "HTTP", "parameters": {"url": "https://x"}}},
132
+ ]
133
+ edges = [
134
+ _edge("ab-1", "agent-1", "input-tools"),
135
+ _edge("tool-1", "agent-1", "input-tools"),
136
+ ]
137
+ node = ab.AgentBuilderNode()
138
+ ctx = _make_ctx(nodes=nodes, edges=edges)
139
+ params = ab.AgentBuilderParams(operation="inspect_canvas")
140
+
141
+ result = await node.inspect_canvas(ctx, params)
142
+
143
+ assert result.operation == "inspect_canvas"
144
+ assert len(result.nodes) == 3
145
+ assert len(result.edges) == 2
146
+ assert result.you["node_id"] == "agent-1" # caller resolved via input-tools edge
147
+ # Both agentBuilder and httpRequest are wired to agent-1's input-tools.
148
+ assert "2 tool(s) wired" in result.summary
149
+ assert "httpRequest" in result.summary
150
+
151
+ async def test_falls_back_to_self_when_no_caller_wired(self):
152
+ node = ab.AgentBuilderNode()
153
+ ctx = _make_ctx(nodes=[], edges=[], self_id="ab-1")
154
+ params = ab.AgentBuilderParams(operation="inspect_canvas")
155
+
156
+ result = await node.inspect_canvas(ctx, params)
157
+
158
+ assert result.you["node_id"] == "ab-1"
159
+
160
+
161
+ # ============================================================================
162
+ # add_tool
163
+ # ============================================================================
164
+
165
+
166
+ class TestAddTool:
167
+ async def test_rejects_empty_node_type(self):
168
+ node = ab.AgentBuilderNode()
169
+ ctx = _make_ctx()
170
+ params = ab.AgentBuilderParams(operation="add_tool", node_type="")
171
+
172
+ result = await node.add_tool(ctx, params)
173
+
174
+ assert result.operations == []
175
+ assert "node_type is required" in result.summary
176
+
177
+ async def test_rejects_disallowed_type(self):
178
+ node = ab.AgentBuilderNode()
179
+ ctx = _make_ctx()
180
+ params = ab.AgentBuilderParams(operation="add_tool", node_type="unknownTool")
181
+
182
+ with patch.object(ab, "registered_node_classes",
183
+ return_value=_registry(httpRequest="tool")):
184
+ result = await node.add_tool(ctx, params)
185
+
186
+ assert result.operations == []
187
+ assert "not an allowed tool type" in result.summary
188
+
189
+ async def test_rejects_self_and_master_skill(self):
190
+ node = ab.AgentBuilderNode()
191
+ ctx = _make_ctx()
192
+
193
+ with patch.object(ab, "registered_node_classes",
194
+ return_value=_registry(
195
+ agentBuilder="tool", masterSkill="tool",
196
+ httpRequest="tool")):
197
+ for forbidden in ("agentBuilder", "masterSkill"):
198
+ params = ab.AgentBuilderParams(operation="add_tool", node_type=forbidden)
199
+ result = await node.add_tool(ctx, params)
200
+ assert result.operations == []
201
+ assert "not an allowed tool type" in result.summary
202
+
203
+ async def test_emits_add_node_and_add_edge_ops(self):
204
+ node = ab.AgentBuilderNode()
205
+ edges = [_edge("ab-1", "agent-1", "input-tools")]
206
+ ctx = _make_ctx(edges=edges)
207
+ params = ab.AgentBuilderParams(operation="add_tool", node_type="httpRequest")
208
+
209
+ with patch.object(ab, "registered_node_classes",
210
+ return_value=_registry(httpRequest="tool")), \
211
+ patch.object(ab, "_broadcast", new_callable=AsyncMock) as mock_bcast:
212
+ result = await node.add_tool(ctx, params)
213
+
214
+ assert len(result.operations) == 2
215
+ assert result.operations[0]["type"] == "add_node"
216
+ assert result.operations[0]["node_type"] == "httpRequest"
217
+ assert result.operations[1]["type"] == "add_edge"
218
+ assert result.operations[1]["target"] == "agent-1"
219
+ assert result.operations[1]["target_handle"] == "input-tools"
220
+ mock_bcast.assert_awaited_once()
221
+ assert "Available on your next turn" in result.summary
222
+
223
+
224
+ # ============================================================================
225
+ # add_skill
226
+ # ============================================================================
227
+
228
+
229
+ class TestAddSkill:
230
+ async def test_rejects_empty_skill_folder(self):
231
+ node = ab.AgentBuilderNode()
232
+ ctx = _make_ctx()
233
+ params = ab.AgentBuilderParams(operation="add_skill", skill_folder="")
234
+
235
+ result = await node.add_skill(ctx, params)
236
+
237
+ assert result.operations == []
238
+ assert "skill_folder is required" in result.summary
239
+
240
+ async def test_rejects_unknown_skill_folder(self):
241
+ node = ab.AgentBuilderNode()
242
+ ctx = _make_ctx()
243
+ params = ab.AgentBuilderParams(
244
+ operation="add_skill", skill_folder="nonexistent-skill",
245
+ )
246
+
247
+ with patch.object(ab, "_skill_folder_exists", return_value=False):
248
+ result = await node.add_skill(ctx, params)
249
+
250
+ assert result.operations == []
251
+ assert "not found" in result.summary
252
+
253
+ async def test_toggles_on_existing_master_skill(self):
254
+ master = {
255
+ "id": "ms-1", "type": "masterSkill",
256
+ "data": {"parameters": {"skills_config": {"old-skill": {"enabled": True}}}},
257
+ }
258
+ agent = _agent_node("agent-1")
259
+ edges = [
260
+ _edge("ab-1", "agent-1", "input-tools"),
261
+ _edge("ms-1", "agent-1", "input-skill"),
262
+ ]
263
+ node = ab.AgentBuilderNode()
264
+ ctx = _make_ctx(nodes=[agent, master], edges=edges)
265
+ params = ab.AgentBuilderParams(
266
+ operation="add_skill", skill_folder="http-request-skill",
267
+ )
268
+
269
+ with patch.object(ab, "_skill_folder_exists", return_value=True), \
270
+ patch.object(ab, "_broadcast", new_callable=AsyncMock):
271
+ result = await node.add_skill(ctx, params)
272
+
273
+ assert len(result.operations) == 1
274
+ op = result.operations[0]
275
+ assert op["type"] == "set_node_parameters"
276
+ assert op["node_id"] == "ms-1"
277
+ new_cfg = op["parameters"]["skills_config"]
278
+ assert new_cfg["http-request-skill"]["enabled"] is True
279
+ assert new_cfg["old-skill"]["enabled"] is True # preserved
280
+
281
+ async def test_spawns_master_skill_when_absent(self):
282
+ agent = _agent_node("agent-1")
283
+ edges = [_edge("ab-1", "agent-1", "input-tools")]
284
+ node = ab.AgentBuilderNode()
285
+ ctx = _make_ctx(nodes=[agent], edges=edges)
286
+ params = ab.AgentBuilderParams(
287
+ operation="add_skill", skill_folder="memory-skill",
288
+ )
289
+
290
+ with patch.object(ab, "_skill_folder_exists", return_value=True), \
291
+ patch.object(ab, "_broadcast", new_callable=AsyncMock):
292
+ result = await node.add_skill(ctx, params)
293
+
294
+ assert len(result.operations) == 2
295
+ assert result.operations[0]["type"] == "add_node"
296
+ assert result.operations[0]["node_type"] == "masterSkill"
297
+ seeded = result.operations[0]["parameters"]["skills_config"]
298
+ assert seeded["memory-skill"]["enabled"] is True
299
+ assert result.operations[1]["type"] == "add_edge"
300
+
301
+
302
+ # ============================================================================
303
+ # add_subagent
304
+ # ============================================================================
305
+
306
+
307
+ class TestAddSubagent:
308
+ async def test_rejects_empty_agent_type(self):
309
+ node = ab.AgentBuilderNode()
310
+ ctx = _make_ctx()
311
+ params = ab.AgentBuilderParams(operation="add_subagent", agent_type="")
312
+
313
+ result = await node.add_subagent(ctx, params)
314
+
315
+ assert result.operations == []
316
+ assert "agent_type is required" in result.summary
317
+
318
+ async def test_rejects_when_caller_is_not_team_lead(self):
319
+ agent = _agent_node("agent-1", "aiAgent") # aiAgent is NOT a team-lead
320
+ edges = [_edge("ab-1", "agent-1", "input-tools")]
321
+ node = ab.AgentBuilderNode()
322
+ ctx = _make_ctx(nodes=[agent], edges=edges)
323
+ params = ab.AgentBuilderParams(
324
+ operation="add_subagent", agent_type="coding_agent",
325
+ )
326
+
327
+ with patch.object(ab, "registered_node_classes",
328
+ return_value=_registry(coding_agent="agent")):
329
+ result = await node.add_subagent(ctx, params)
330
+
331
+ assert result.operations == []
332
+ assert "team-lead" in result.summary
333
+
334
+ async def test_rejects_disallowed_agent_type(self):
335
+ agent = _agent_node("agent-1", "orchestrator_agent")
336
+ edges = [_edge("ab-1", "agent-1", "input-tools")]
337
+ node = ab.AgentBuilderNode()
338
+ ctx = _make_ctx(nodes=[agent], edges=edges)
339
+ params = ab.AgentBuilderParams(
340
+ operation="add_subagent", agent_type="not_a_real_agent",
341
+ )
342
+
343
+ with patch.object(ab, "registered_node_classes",
344
+ return_value=_registry(coding_agent="agent")):
345
+ result = await node.add_subagent(ctx, params)
346
+
347
+ assert result.operations == []
348
+ assert "not an allowed agent type" in result.summary
349
+
350
+ async def test_rejects_spawning_another_team_lead(self):
351
+ agent = _agent_node("agent-1", "orchestrator_agent")
352
+ edges = [_edge("ab-1", "agent-1", "input-tools")]
353
+ node = ab.AgentBuilderNode()
354
+ ctx = _make_ctx(nodes=[agent], edges=edges)
355
+ params = ab.AgentBuilderParams(
356
+ operation="add_subagent", agent_type="ai_employee",
357
+ )
358
+
359
+ with patch.object(ab, "registered_node_classes",
360
+ return_value=_registry(ai_employee="agent")):
361
+ result = await node.add_subagent(ctx, params)
362
+
363
+ assert result.operations == []
364
+ assert "cannot spawn another team-lead" in result.summary
365
+
366
+ async def test_emits_add_node_and_teammate_edge(self):
367
+ agent = _agent_node("agent-1", "orchestrator_agent")
368
+ edges = [_edge("ab-1", "agent-1", "input-tools")]
369
+ node = ab.AgentBuilderNode()
370
+ ctx = _make_ctx(nodes=[agent], edges=edges)
371
+ params = ab.AgentBuilderParams(
372
+ operation="add_subagent", agent_type="coding_agent",
373
+ )
374
+
375
+ with patch.object(ab, "registered_node_classes",
376
+ return_value=_registry(coding_agent="agent")), \
377
+ patch.object(ab, "_broadcast", new_callable=AsyncMock):
378
+ result = await node.add_subagent(ctx, params)
379
+
380
+ assert len(result.operations) == 2
381
+ assert result.operations[0]["node_type"] == "coding_agent"
382
+ assert result.operations[1]["target_handle"] == "input-teammates"
383
+
384
+
385
+ # ============================================================================
386
+ # create_workflow
387
+ # ============================================================================
388
+
389
+
390
+ class TestCreateWorkflow:
391
+ async def test_rejects_empty_name(self):
392
+ node = ab.AgentBuilderNode()
393
+ ctx = _make_ctx()
394
+ params = ab.AgentBuilderParams(operation="create_workflow", workflow_name="")
395
+
396
+ result = await node.create_workflow(ctx, params)
397
+
398
+ assert result.workflow_id is None
399
+ assert "workflow_name is required" in result.summary
400
+
401
+ async def test_persists_via_database_and_returns_id(self):
402
+ node = ab.AgentBuilderNode()
403
+ ctx = _make_ctx()
404
+ params = ab.AgentBuilderParams(
405
+ operation="create_workflow",
406
+ workflow_name="My New Workflow",
407
+ workflow_description="An optional description",
408
+ )
409
+
410
+ mock_db = MagicMock()
411
+ mock_db.save_workflow = AsyncMock(return_value=True)
412
+ mock_container = MagicMock()
413
+ mock_container.database.return_value = mock_db
414
+
415
+ with patch("core.container.container", mock_container):
416
+ result = await node.create_workflow(ctx, params)
417
+
418
+ assert result.workflow_id is not None
419
+ assert result.workflow_id.startswith("wf_")
420
+ assert "My New Workflow" in result.summary
421
+ mock_db.save_workflow.assert_awaited_once()
422
+
423
+ async def test_returns_failure_summary_when_persist_fails(self):
424
+ node = ab.AgentBuilderNode()
425
+ ctx = _make_ctx()
426
+ params = ab.AgentBuilderParams(
427
+ operation="create_workflow", workflow_name="Doomed Workflow",
428
+ )
429
+
430
+ mock_db = MagicMock()
431
+ mock_db.save_workflow = AsyncMock(return_value=False)
432
+ mock_container = MagicMock()
433
+ mock_container.database.return_value = mock_db
434
+
435
+ with patch("core.container.container", mock_container):
436
+ result = await node.create_workflow(ctx, params)
437
+
438
+ assert result.workflow_id is None
439
+ assert "failed to persist" in result.summary
@@ -180,8 +180,9 @@ class TestDuckDuckGoSearch:
180
180
  fake_module = MagicMock()
181
181
  fake_module.DDGS = fake_ddgs_class
182
182
 
183
+ from tests.nodes._compat import _execute_duckduckgo_search as _flat_search
183
184
  with patch.dict("sys.modules", {"ddgs": fake_module}):
184
- result = await tools_mod._execute_duckduckgo_search(
185
+ result = await _flat_search(
185
186
  {"query": "python"},
186
187
  {"provider": "duckduckgo", "max_results": 2},
187
188
  )
@@ -201,7 +202,7 @@ class TestDuckDuckGoSearch:
201
202
  assert result == {"error": "No search query provided"}
202
203
 
203
204
  async def test_missing_keys_in_ddgs_output_default_to_empty_string(self):
204
- from services.handlers import tools as tools_mod
205
+ from tests.nodes._compat import _execute_duckduckgo_search as _flat_search
205
206
 
206
207
  fake_ddgs_instance = MagicMock()
207
208
  fake_ddgs_instance.text.return_value = [{"title": "only-title"}]
@@ -209,15 +210,12 @@ class TestDuckDuckGoSearch:
209
210
  fake_module.DDGS = MagicMock(return_value=fake_ddgs_instance)
210
211
 
211
212
  with patch.dict("sys.modules", {"ddgs": fake_module}):
212
- result = await tools_mod._execute_duckduckgo_search(
213
- {"query": "q"},
214
- {"max_results": 5},
215
- )
213
+ result = await _flat_search({"query": "q"}, {"max_results": 5})
216
214
 
217
215
  assert result["results"] == [{"title": "only-title", "snippet": "", "url": ""}]
218
216
 
219
217
  async def test_max_results_defaults_to_5_when_missing(self):
220
- from services.handlers import tools as tools_mod
218
+ from tests.nodes._compat import _execute_duckduckgo_search as _flat_search
221
219
 
222
220
  fake_ddgs_instance = MagicMock()
223
221
  fake_ddgs_instance.text.return_value = []
@@ -225,7 +223,7 @@ class TestDuckDuckGoSearch:
225
223
  fake_module.DDGS = MagicMock(return_value=fake_ddgs_instance)
226
224
 
227
225
  with patch.dict("sys.modules", {"ddgs": fake_module}):
228
- await tools_mod._execute_duckduckgo_search({"query": "q"}, {})
226
+ await _flat_search({"query": "q"}, {})
229
227
 
230
228
  fake_ddgs_instance.text.assert_called_once_with("q", max_results=5)
231
229
 
@@ -269,7 +267,8 @@ class TestTaskManager:
269
267
  "result": "x" * 500, # will be truncated to 200 chars
270
268
  }
271
269
 
272
- result = await tools_mod._execute_task_manager(
270
+ from nodes.tool.task_manager import _execute_task_manager
271
+ result = await _execute_task_manager(
273
272
  {"operation": "list_tasks"}, {"parameters": {}}
274
273
  )
275
274
 
@@ -294,7 +293,8 @@ class TestTaskManager:
294
293
  tools_mod._delegated_tasks["r1"] = running
295
294
  tools_mod._delegation_results["c1"] = {"status": "completed"}
296
295
 
297
- result = await tools_mod._execute_task_manager(
296
+ from nodes.tool.task_manager import _execute_task_manager
297
+ result = await _execute_task_manager(
298
298
  {"operation": "list_tasks", "status_filter": "running"},
299
299
  {"parameters": {}},
300
300
  )
@@ -305,7 +305,8 @@ class TestTaskManager:
305
305
  async def test_get_task_without_id_errors(self, _reset_registry):
306
306
  tools_mod = _reset_registry
307
307
 
308
- result = await tools_mod._execute_task_manager(
308
+ from nodes.tool.task_manager import _execute_task_manager
309
+ result = await _execute_task_manager(
309
310
  {"operation": "get_task"}, {"parameters": {}}
310
311
  )
311
312
  assert result["success"] is False
@@ -315,7 +316,8 @@ class TestTaskManager:
315
316
  tools_mod = _reset_registry
316
317
  tools_mod._delegation_results["gone"] = {"status": "completed"}
317
318
 
318
- result = await tools_mod._execute_task_manager(
319
+ from nodes.tool.task_manager import _execute_task_manager
320
+ result = await _execute_task_manager(
319
321
  {"operation": "mark_done", "task_id": "gone"},
320
322
  {"parameters": {}},
321
323
  )
@@ -327,7 +329,8 @@ class TestTaskManager:
327
329
  async def test_mark_done_untracked_id_returns_removed_false(self, _reset_registry):
328
330
  tools_mod = _reset_registry
329
331
 
330
- result = await tools_mod._execute_task_manager(
332
+ from nodes.tool.task_manager import _execute_task_manager
333
+ result = await _execute_task_manager(
331
334
  {"operation": "mark_done", "task_id": "never-seen"},
332
335
  {"parameters": {}},
333
336
  )
@@ -339,7 +342,8 @@ class TestTaskManager:
339
342
  async def test_unknown_operation_returns_failure_envelope(self, _reset_registry):
340
343
  tools_mod = _reset_registry
341
344
 
342
- result = await tools_mod._execute_task_manager(
345
+ from nodes.tool.task_manager import _execute_task_manager
346
+ result = await _execute_task_manager(
343
347
  {"operation": "self_destruct"}, {"parameters": {}}
344
348
  )
345
349
  assert result["success"] is False
@@ -313,8 +313,10 @@ class TestFileRead:
313
313
  harness.assert_output_shape(result, ["content", "file_path"])
314
314
  payload = result["result"]
315
315
  assert payload["content"] == "line1\nline2\n"
316
- assert payload["file_path"] == "notes.txt"
317
- backend.read.assert_called_once_with("notes.txt", offset=0, limit=100)
316
+ # ``normalize_virtual_path`` prepends ``/`` to relative inputs so the
317
+ # path reaches deepagents in its canonical virtual-mode form.
318
+ assert payload["file_path"] == "/notes.txt"
319
+ backend.read.assert_called_once_with("/notes.txt", offset=0, limit=100)
318
320
 
319
321
  async def test_missing_file_path_short_circuits(self, harness):
320
322
  # No backend patch: we should short-circuit before reaching it.
@@ -333,7 +335,10 @@ class TestFileRead:
333
335
  )
334
336
 
335
337
  harness.assert_envelope(result, success=False)
336
- assert "no such file" in result["error"].lower()
338
+ # ``normalize_virtual_path`` rejects ``..`` segments before the
339
+ # backend is even called, so the error comes from deepagents'
340
+ # ``validate_path`` helper, not the (unreached) FileNotFoundError.
341
+ assert "path traversal not allowed" in result["error"].lower()
337
342
 
338
343
 
339
344
  # ============================================================================
@@ -344,7 +349,11 @@ class TestFileRead:
344
349
  class TestFileModify:
345
350
  async def test_write_happy_path(self, harness):
346
351
  backend = MagicMock(name="LocalShellBackend")
347
- backend.write = MagicMock(return_value=_FakeWriteResult(path="hello.txt"))
352
+ backend.write = MagicMock(return_value=_FakeWriteResult(path="/hello.txt"))
353
+ # ``write`` now does a pre-flight ``backend._resolve_path(...).exists()``
354
+ # check so it can unlink-and-replace; teach the mock that the target
355
+ # doesn't exist yet so the wholesale-write path proceeds.
356
+ backend._resolve_path.return_value.exists.return_value = False
348
357
 
349
358
  with _patch_fs_backend(backend):
350
359
  result = await harness.execute(
@@ -359,8 +368,8 @@ class TestFileModify:
359
368
  harness.assert_envelope(result, success=True)
360
369
  harness.assert_output_shape(result, ["operation", "file_path"])
361
370
  assert result["result"]["operation"] == "write"
362
- assert result["result"]["file_path"] == "hello.txt"
363
- backend.write.assert_called_once_with("hello.txt", "hi there")
371
+ assert result["result"]["file_path"] == "/hello.txt"
372
+ backend.write.assert_called_once_with("/hello.txt", "hi there")
364
373
 
365
374
  async def test_edit_happy_path_returns_occurrences(self, harness):
366
375
  backend = MagicMock(name="LocalShellBackend")
@@ -386,7 +395,7 @@ class TestFileModify:
386
395
  )
387
396
  assert result["result"]["occurrences"] == 3
388
397
  backend.edit.assert_called_once_with(
389
- "README.md", "foo", "bar", replace_all=True
398
+ "/README.md", "foo", "bar", replace_all=True
390
399
  )
391
400
 
392
401
  async def test_edit_missing_old_string_short_circuits(self, harness):
@@ -559,7 +568,7 @@ class TestFsSearch:
559
568
  result, ["path", "pattern", "matches", "count"]
560
569
  )
561
570
  assert result["result"]["count"] == 1
562
- backend.glob_info.assert_called_once_with("**/*.py", path="src")
571
+ backend.glob_info.assert_called_once_with("**/*.py", path="/src")
563
572
 
564
573
  async def test_grep_returns_string_error_as_error_envelope(self, harness):
565
574
  # Per doc: grep_raw returns a str on error, list on success.