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