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,692 @@
|
|
|
1
|
+
"""Wave 12 C1 canary: DeploymentManager Temporal-listener integration tests.
|
|
2
|
+
|
|
3
|
+
These tests exercise the canary plumbing inside :class:`DeploymentManager`
|
|
4
|
+
WITHOUT a live Temporal cluster — the ``container.temporal_client()``
|
|
5
|
+
call is monkeypatched to return a stub whose ``client.start_workflow``
|
|
6
|
+
and ``client.list_workflows`` calls are captured and asserted.
|
|
7
|
+
|
|
8
|
+
The canonical pattern under test:
|
|
9
|
+
|
|
10
|
+
1. **Deterministic listener id** = ``trigger-listener-{workflow_id}-{node_id}``.
|
|
11
|
+
Re-deploy of the same MachinaOs workflow targets the same Temporal id.
|
|
12
|
+
2. **WorkflowIDConflictPolicy.USE_EXISTING** — Temporal returns the existing
|
|
13
|
+
handle instead of erroring on a duplicate-id start. Idempotent re-deploy.
|
|
14
|
+
3. **TypedSearchAttributes set at start** — ``EventType``, ``TriggerNodeId``,
|
|
15
|
+
``EventWorkflowId``, ``EventTriggerKind``. These ARE the registry the
|
|
16
|
+
cancel path queries.
|
|
17
|
+
4. **Cancel via ``list_workflows(query=...) + handle.cancel()``** — no
|
|
18
|
+
instance-state dict, no handle caching. The Temporal server's
|
|
19
|
+
Visibility store is the source of truth.
|
|
20
|
+
|
|
21
|
+
Cross-confirmed across three official sources (Temporal docs, samples-python,
|
|
22
|
+
Inngest/Prefect/n8n source analysis). Locking the implementation against
|
|
23
|
+
these invariants here prevents future drift back to tribal in-memory tracking.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
import types
|
|
30
|
+
from typing import Any, Dict, List
|
|
31
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
32
|
+
|
|
33
|
+
import pytest
|
|
34
|
+
|
|
35
|
+
# Stub `machina` namespace — same pattern as other event-framework tests.
|
|
36
|
+
if "machina" not in sys.modules:
|
|
37
|
+
_machina = types.ModuleType("cli")
|
|
38
|
+
_machina.__path__ = []
|
|
39
|
+
sys.modules["cli"] = _machina
|
|
40
|
+
_machina_tcp = types.ModuleType("cli.tcp")
|
|
41
|
+
_machina_tcp.probe_tcp_port = MagicMock(return_value=False)
|
|
42
|
+
sys.modules["cli.tcp"] = _machina_tcp
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def fresh_canary_registry(monkeypatch):
|
|
47
|
+
"""Reset the canary-registry backing store to a known empty state.
|
|
48
|
+
|
|
49
|
+
The production registry accumulates as plugin modules import — that's
|
|
50
|
+
correct for runtime. Tests that assert canary scope must isolate
|
|
51
|
+
from accumulating module state so a future plugin opt-in doesn't
|
|
52
|
+
silently flip a test outcome. This fixture monkeypatches the
|
|
53
|
+
underlying ``_REGISTERED`` set; ``monkeypatch`` restores it after
|
|
54
|
+
the test.
|
|
55
|
+
"""
|
|
56
|
+
from services.deployment import canary_registry
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(canary_registry, "_REGISTERED", {})
|
|
59
|
+
return canary_registry
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Helpers — build a DeploymentManager with the minimum scaffolding to drive
|
|
64
|
+
# the canary code paths. Database / status broadcaster are pure mocks.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_manager_with_state(workflow_id: str, nodes, edges, session_id="sess"):
|
|
69
|
+
"""Build a DeploymentManager populated with one in-flight deployment.
|
|
70
|
+
|
|
71
|
+
The canary methods read ``self._deployments[workflow_id]`` for nodes/edges
|
|
72
|
+
snapshots, so we register that state explicitly.
|
|
73
|
+
"""
|
|
74
|
+
from services.deployment.manager import DeploymentManager
|
|
75
|
+
from services.deployment.state import DeploymentState
|
|
76
|
+
|
|
77
|
+
database = MagicMock()
|
|
78
|
+
database.get_node_parameters = AsyncMock(return_value={})
|
|
79
|
+
broadcaster = MagicMock()
|
|
80
|
+
broadcaster.update_node_status = AsyncMock()
|
|
81
|
+
|
|
82
|
+
mgr = DeploymentManager(
|
|
83
|
+
database=database,
|
|
84
|
+
execute_workflow_fn=AsyncMock(return_value={"success": True}),
|
|
85
|
+
store_output_fn=AsyncMock(),
|
|
86
|
+
broadcaster=broadcaster,
|
|
87
|
+
)
|
|
88
|
+
mgr._deployments[workflow_id] = DeploymentState(
|
|
89
|
+
deployment_id=f"deploy_{workflow_id}",
|
|
90
|
+
workflow_id=workflow_id,
|
|
91
|
+
is_running=True,
|
|
92
|
+
nodes=nodes,
|
|
93
|
+
edges=edges,
|
|
94
|
+
session_id=session_id,
|
|
95
|
+
)
|
|
96
|
+
return mgr, broadcaster
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _node(node_id: str, node_type: str) -> Dict[str, Any]:
|
|
100
|
+
return {"id": node_id, "type": node_type, "data": {}}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# C1d.1 — deterministic listener id
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestDeterministicListenerId:
|
|
109
|
+
"""Re-deploy targets the same Temporal id; downstream USE_EXISTING
|
|
110
|
+
policy handles the duplicate gracefully."""
|
|
111
|
+
|
|
112
|
+
def test_id_format_is_stable(self):
|
|
113
|
+
from services.deployment.manager import DeploymentManager
|
|
114
|
+
|
|
115
|
+
first = DeploymentManager._listener_workflow_id("wf-1", "wh-1")
|
|
116
|
+
second = DeploymentManager._listener_workflow_id("wf-1", "wh-1")
|
|
117
|
+
assert first == second == "trigger-listener-wf-1-wh-1"
|
|
118
|
+
|
|
119
|
+
def test_id_differs_per_node_in_same_workflow(self):
|
|
120
|
+
from services.deployment.manager import DeploymentManager
|
|
121
|
+
|
|
122
|
+
a = DeploymentManager._listener_workflow_id("wf-1", "wh-1")
|
|
123
|
+
b = DeploymentManager._listener_workflow_id("wf-1", "wh-2")
|
|
124
|
+
assert a != b
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# C1d.2 — canary scope: flag + type
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestCanaryScope:
|
|
133
|
+
"""Canary only activates for trigger types opted in via
|
|
134
|
+
:func:`register_canary_trigger_type` AND when the feature flag is on.
|
|
135
|
+
Uses ``fresh_canary_registry`` so each test starts with an empty
|
|
136
|
+
registry — no cross-test pollution from accumulated plugin imports."""
|
|
137
|
+
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_disabled_when_flag_off(self, monkeypatch, fresh_canary_registry):
|
|
140
|
+
from services.deployment.manager import DeploymentManager
|
|
141
|
+
|
|
142
|
+
fresh_canary_registry.register_canary_trigger_type("webhookTrigger", "com.machinaos.webhook.received")
|
|
143
|
+
|
|
144
|
+
class _Off:
|
|
145
|
+
event_framework_enabled = False
|
|
146
|
+
|
|
147
|
+
import core.config
|
|
148
|
+
|
|
149
|
+
monkeypatch.setattr(core.config, "Settings", lambda: _Off())
|
|
150
|
+
|
|
151
|
+
result = await DeploymentManager._canary_listener_enabled_for("webhookTrigger")
|
|
152
|
+
assert result is False
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_disabled_for_unregistered_type(self, monkeypatch, fresh_canary_registry):
|
|
156
|
+
"""Plugin must opt in via register_canary_trigger_type; types
|
|
157
|
+
not in the registry stay on the legacy path even with the flag on."""
|
|
158
|
+
from services.deployment.manager import DeploymentManager
|
|
159
|
+
|
|
160
|
+
# Registry is empty (fresh fixture); whatsappReceive not registered.
|
|
161
|
+
class _On:
|
|
162
|
+
event_framework_enabled = True
|
|
163
|
+
|
|
164
|
+
import core.config
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr(core.config, "Settings", lambda: _On())
|
|
167
|
+
|
|
168
|
+
result = await DeploymentManager._canary_listener_enabled_for("whatsappReceive")
|
|
169
|
+
assert result is False
|
|
170
|
+
|
|
171
|
+
@pytest.mark.asyncio
|
|
172
|
+
async def test_enabled_when_registered_and_flag_on(self, monkeypatch, fresh_canary_registry):
|
|
173
|
+
from services.deployment.manager import DeploymentManager
|
|
174
|
+
|
|
175
|
+
fresh_canary_registry.register_canary_trigger_type("webhookTrigger", "com.machinaos.webhook.received")
|
|
176
|
+
fresh_canary_registry.register_canary_trigger_type("chatTrigger", "com.machinaos.chat.message.received")
|
|
177
|
+
|
|
178
|
+
class _On:
|
|
179
|
+
event_framework_enabled = True
|
|
180
|
+
|
|
181
|
+
import core.config
|
|
182
|
+
|
|
183
|
+
monkeypatch.setattr(core.config, "Settings", lambda: _On())
|
|
184
|
+
|
|
185
|
+
assert await DeploymentManager._canary_listener_enabled_for("webhookTrigger") is True
|
|
186
|
+
assert await DeploymentManager._canary_listener_enabled_for("chatTrigger") is True
|
|
187
|
+
# Sanity: still respects the registry boundary.
|
|
188
|
+
assert await DeploymentManager._canary_listener_enabled_for("randomThing") is False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestTriggerKindDerivation:
|
|
192
|
+
"""``EventTriggerKind`` Search Attribute is derived from node_type
|
|
193
|
+
by stripping the ``Trigger`` / ``Receive`` suffix. Locks the mapping
|
|
194
|
+
so future trigger types (e.g. ``slackReceive``) get a sensible
|
|
195
|
+
default kind without per-type code changes in DeploymentManager."""
|
|
196
|
+
|
|
197
|
+
def test_strips_trigger_suffix(self):
|
|
198
|
+
from services.deployment.manager import DeploymentManager
|
|
199
|
+
|
|
200
|
+
assert DeploymentManager._trigger_kind_for("webhookTrigger") == "webhook"
|
|
201
|
+
assert DeploymentManager._trigger_kind_for("chatTrigger") == "chat"
|
|
202
|
+
assert DeploymentManager._trigger_kind_for("taskTrigger") == "task"
|
|
203
|
+
|
|
204
|
+
def test_strips_receive_suffix(self):
|
|
205
|
+
from services.deployment.manager import DeploymentManager
|
|
206
|
+
|
|
207
|
+
assert DeploymentManager._trigger_kind_for("telegramReceive") == "telegram"
|
|
208
|
+
assert DeploymentManager._trigger_kind_for("whatsappReceive") == "whatsapp"
|
|
209
|
+
assert DeploymentManager._trigger_kind_for("emailReceive") == "email"
|
|
210
|
+
|
|
211
|
+
def test_unknown_suffix_returns_node_type_verbatim(self):
|
|
212
|
+
from services.deployment.manager import DeploymentManager
|
|
213
|
+
|
|
214
|
+
# Defensive fallback — never crashes, just emits whatever the
|
|
215
|
+
# node_type was. Ops dashboards see the raw value and can
|
|
216
|
+
# update the chip-mapping when needed.
|
|
217
|
+
assert DeploymentManager._trigger_kind_for("customListener") == "customListener"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# C1d.3 — _start_canary_listener: deterministic id + USE_EXISTING + search attrs
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestStartCanaryListener:
|
|
226
|
+
"""The canonical start-workflow shape: deterministic id +
|
|
227
|
+
WorkflowIDConflictPolicy.USE_EXISTING + TypedSearchAttributes."""
|
|
228
|
+
|
|
229
|
+
@pytest.mark.asyncio
|
|
230
|
+
async def test_starts_with_deterministic_id_and_use_existing_policy(self, monkeypatch):
|
|
231
|
+
from temporalio.common import WorkflowIDConflictPolicy
|
|
232
|
+
|
|
233
|
+
mgr, _ = _build_manager_with_state(
|
|
234
|
+
"wf-abc",
|
|
235
|
+
nodes=[_node("wh-1", "webhookTrigger"), _node("agent-1", "aiAgent")],
|
|
236
|
+
edges=[{"source": "wh-1", "target": "agent-1", "targetHandle": "input-main"}],
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
recorded_start: List[Dict[str, Any]] = []
|
|
240
|
+
|
|
241
|
+
async def fake_start_workflow(workflow_name, **kwargs):
|
|
242
|
+
recorded_start.append({"name": workflow_name, **kwargs})
|
|
243
|
+
return MagicMock()
|
|
244
|
+
|
|
245
|
+
client = MagicMock()
|
|
246
|
+
client.start_workflow = fake_start_workflow
|
|
247
|
+
|
|
248
|
+
wrapper = MagicMock()
|
|
249
|
+
wrapper.client = client
|
|
250
|
+
|
|
251
|
+
from core import container as container_mod
|
|
252
|
+
|
|
253
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
254
|
+
|
|
255
|
+
node = _node("wh-1", "webhookTrigger")
|
|
256
|
+
listener_id = await mgr._start_canary_listener(node, "wf-abc", params={"path": "hook"})
|
|
257
|
+
|
|
258
|
+
assert listener_id == "trigger-listener-wf-abc-wh-1"
|
|
259
|
+
assert len(recorded_start) == 1
|
|
260
|
+
call = recorded_start[0]
|
|
261
|
+
assert call["name"] == "TriggerListenerWorkflow"
|
|
262
|
+
assert call["id"] == "trigger-listener-wf-abc-wh-1"
|
|
263
|
+
assert call["id_conflict_policy"] == WorkflowIDConflictPolicy.USE_EXISTING
|
|
264
|
+
assert call["task_queue"] == "machina-tasks"
|
|
265
|
+
|
|
266
|
+
@pytest.mark.asyncio
|
|
267
|
+
async def test_search_attributes_include_event_workflow_id(self, monkeypatch):
|
|
268
|
+
"""Cancel path queries by EventWorkflowId — start must set it."""
|
|
269
|
+
from temporalio.common import (
|
|
270
|
+
TypedSearchAttributes,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
mgr, _ = _build_manager_with_state(
|
|
274
|
+
"wf-xyz",
|
|
275
|
+
nodes=[_node("wh-1", "webhookTrigger")],
|
|
276
|
+
edges=[],
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
recorded: List[Dict[str, Any]] = []
|
|
280
|
+
|
|
281
|
+
async def fake_start_workflow(workflow_name, **kwargs):
|
|
282
|
+
recorded.append(kwargs)
|
|
283
|
+
return MagicMock()
|
|
284
|
+
|
|
285
|
+
client = MagicMock()
|
|
286
|
+
client.start_workflow = fake_start_workflow
|
|
287
|
+
wrapper = MagicMock()
|
|
288
|
+
wrapper.client = client
|
|
289
|
+
|
|
290
|
+
from core import container as container_mod
|
|
291
|
+
|
|
292
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
293
|
+
|
|
294
|
+
await mgr._start_canary_listener(_node("wh-1", "webhookTrigger"), "wf-xyz", params={})
|
|
295
|
+
|
|
296
|
+
sa = recorded[0]["search_attributes"]
|
|
297
|
+
assert isinstance(sa, TypedSearchAttributes)
|
|
298
|
+
|
|
299
|
+
# Extract by key — TypedSearchAttributes is iterable over Pair objects.
|
|
300
|
+
attrs_by_name = {pair.key.name: pair.value for pair in sa}
|
|
301
|
+
assert attrs_by_name["EventWorkflowId"] == "wf-xyz"
|
|
302
|
+
assert attrs_by_name["TriggerNodeId"] == "wh-1"
|
|
303
|
+
# EventTriggerKind derived via _trigger_kind_for (strips "Trigger" /
|
|
304
|
+
# "Receive" suffix) — NOT hardcoded "webhook".
|
|
305
|
+
assert attrs_by_name["EventTriggerKind"] == "webhook"
|
|
306
|
+
# EventType MUST be the CloudEvents reverse-DNS string the
|
|
307
|
+
# producer puts on outgoing envelopes (registered via
|
|
308
|
+
# canary_registry). dispatch.emit's Visibility query substitutes
|
|
309
|
+
# event.type into the EventType filter — if the SA carries the
|
|
310
|
+
# legacy snake_case event_waiter string instead, the query
|
|
311
|
+
# never matches and no signal reaches the listener.
|
|
312
|
+
assert attrs_by_name["EventType"] == "com.machinaos.webhook.received"
|
|
313
|
+
|
|
314
|
+
@pytest.mark.asyncio
|
|
315
|
+
async def test_chat_trigger_uses_chat_kind_in_search_attrs(self, monkeypatch):
|
|
316
|
+
"""C1 rollout #1: starting a chatTrigger listener picks the
|
|
317
|
+
right EventTriggerKind ('chat', not 'webhook')."""
|
|
318
|
+
|
|
319
|
+
mgr, _ = _build_manager_with_state(
|
|
320
|
+
"wf-chat",
|
|
321
|
+
nodes=[_node("ct-1", "chatTrigger"), _node("agent-1", "aiAgent")],
|
|
322
|
+
edges=[{"source": "ct-1", "target": "agent-1", "targetHandle": "input-main"}],
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
recorded: List[Dict[str, Any]] = []
|
|
326
|
+
|
|
327
|
+
async def fake_start_workflow(workflow_name, **kwargs):
|
|
328
|
+
recorded.append(kwargs)
|
|
329
|
+
return MagicMock()
|
|
330
|
+
|
|
331
|
+
client = MagicMock()
|
|
332
|
+
client.start_workflow = fake_start_workflow
|
|
333
|
+
wrapper = MagicMock()
|
|
334
|
+
wrapper.client = client
|
|
335
|
+
|
|
336
|
+
from core import container as container_mod
|
|
337
|
+
|
|
338
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
339
|
+
|
|
340
|
+
listener_id = await mgr._start_canary_listener(_node("ct-1", "chatTrigger"), "wf-chat", params={"session_id": "default"})
|
|
341
|
+
assert listener_id == "trigger-listener-wf-chat-ct-1"
|
|
342
|
+
|
|
343
|
+
sa = recorded[0]["search_attributes"]
|
|
344
|
+
attrs_by_name = {pair.key.name: pair.value for pair in sa}
|
|
345
|
+
assert attrs_by_name["EventTriggerKind"] == "chat"
|
|
346
|
+
# CloudEvents reverse-DNS — see test_search_attributes_include_event_workflow_id
|
|
347
|
+
# for the full rationale.
|
|
348
|
+
assert attrs_by_name["EventType"] == "com.machinaos.chat.message.received"
|
|
349
|
+
|
|
350
|
+
@pytest.mark.asyncio
|
|
351
|
+
async def test_returns_none_when_temporal_not_connected(self, monkeypatch):
|
|
352
|
+
"""Falls through to legacy path; doesn't raise."""
|
|
353
|
+
|
|
354
|
+
mgr, _ = _build_manager_with_state("wf-1", nodes=[_node("wh-1", "webhookTrigger")], edges=[])
|
|
355
|
+
|
|
356
|
+
wrapper = MagicMock()
|
|
357
|
+
wrapper.client = None
|
|
358
|
+
|
|
359
|
+
from core import container as container_mod
|
|
360
|
+
|
|
361
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
362
|
+
|
|
363
|
+
result = await mgr._start_canary_listener(_node("wh-1", "webhookTrigger"), "wf-1", params={})
|
|
364
|
+
assert result is None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
# C1d.4 — _cancel_canary_listeners: Visibility query + handle.cancel()
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class TestCancelCanaryListeners:
|
|
373
|
+
"""Cancel uses Visibility query — NO local dict — and graceful cancel()."""
|
|
374
|
+
|
|
375
|
+
@pytest.mark.asyncio
|
|
376
|
+
async def test_query_filters_by_workflow_id_and_listener_type(self, monkeypatch):
|
|
377
|
+
mgr, _ = _build_manager_with_state("wf-1", nodes=[], edges=[])
|
|
378
|
+
|
|
379
|
+
recorded_queries: List[str] = []
|
|
380
|
+
cancelled_ids: List[str] = []
|
|
381
|
+
|
|
382
|
+
async def fake_list_workflows(query):
|
|
383
|
+
recorded_queries.append(query)
|
|
384
|
+
for wf_id in ["trigger-listener-wf-1-wh-1", "trigger-listener-wf-1-wh-2"]:
|
|
385
|
+
yield MagicMock(id=wf_id)
|
|
386
|
+
|
|
387
|
+
handles_by_id: Dict[str, MagicMock] = {}
|
|
388
|
+
|
|
389
|
+
def fake_get_handle(wf_id):
|
|
390
|
+
handle = MagicMock()
|
|
391
|
+
|
|
392
|
+
async def fake_cancel():
|
|
393
|
+
cancelled_ids.append(wf_id)
|
|
394
|
+
|
|
395
|
+
handle.cancel = fake_cancel
|
|
396
|
+
handles_by_id[wf_id] = handle
|
|
397
|
+
return handle
|
|
398
|
+
|
|
399
|
+
client = MagicMock()
|
|
400
|
+
client.list_workflows = fake_list_workflows
|
|
401
|
+
client.get_workflow_handle = fake_get_handle
|
|
402
|
+
|
|
403
|
+
wrapper = MagicMock()
|
|
404
|
+
wrapper.client = client
|
|
405
|
+
|
|
406
|
+
from core import container as container_mod
|
|
407
|
+
|
|
408
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
409
|
+
|
|
410
|
+
cancelled = await mgr._cancel_canary_listeners("wf-1")
|
|
411
|
+
|
|
412
|
+
# Query shape: EventWorkflowId + WorkflowType IN (...) +
|
|
413
|
+
# ExecutionStatus. The IN clause covers both push
|
|
414
|
+
# (TriggerListenerWorkflow) and polling (PollingTriggerWorkflow)
|
|
415
|
+
# listener workflow types since Wave 12 C2 — deployment cancel
|
|
416
|
+
# drains both in one sweep.
|
|
417
|
+
assert len(recorded_queries) == 1
|
|
418
|
+
q = recorded_queries[0]
|
|
419
|
+
assert "EventWorkflowId='wf-1'" in q
|
|
420
|
+
assert "'TriggerListenerWorkflow'" in q
|
|
421
|
+
assert "'PollingTriggerWorkflow'" in q
|
|
422
|
+
assert "WorkflowType IN" in q
|
|
423
|
+
assert "ExecutionStatus='Running'" in q
|
|
424
|
+
|
|
425
|
+
assert cancelled == 2
|
|
426
|
+
assert sorted(cancelled_ids) == [
|
|
427
|
+
"trigger-listener-wf-1-wh-1",
|
|
428
|
+
"trigger-listener-wf-1-wh-2",
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
@pytest.mark.asyncio
|
|
432
|
+
async def test_zero_listeners_returns_zero(self, monkeypatch):
|
|
433
|
+
"""Visibility query with no results is the steady-state — must not raise."""
|
|
434
|
+
|
|
435
|
+
mgr, _ = _build_manager_with_state("wf-1", nodes=[], edges=[])
|
|
436
|
+
|
|
437
|
+
async def empty_list(query):
|
|
438
|
+
if False:
|
|
439
|
+
yield None # make this an async generator
|
|
440
|
+
|
|
441
|
+
client = MagicMock()
|
|
442
|
+
client.list_workflows = empty_list
|
|
443
|
+
wrapper = MagicMock()
|
|
444
|
+
wrapper.client = client
|
|
445
|
+
|
|
446
|
+
from core import container as container_mod
|
|
447
|
+
|
|
448
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
449
|
+
|
|
450
|
+
cancelled = await mgr._cancel_canary_listeners("wf-1")
|
|
451
|
+
assert cancelled == 0
|
|
452
|
+
|
|
453
|
+
@pytest.mark.asyncio
|
|
454
|
+
async def test_per_listener_failure_doesnt_block_sweep(self, monkeypatch):
|
|
455
|
+
"""One listener failing to cancel shouldn't strand the others —
|
|
456
|
+
each handle.cancel() is wrapped in its own try/except."""
|
|
457
|
+
|
|
458
|
+
mgr, _ = _build_manager_with_state("wf-1", nodes=[], edges=[])
|
|
459
|
+
|
|
460
|
+
cancelled_ids: List[str] = []
|
|
461
|
+
|
|
462
|
+
async def list_two(query):
|
|
463
|
+
for wf_id in ["trigger-listener-wf-1-wh-1", "trigger-listener-wf-1-wh-2"]:
|
|
464
|
+
yield MagicMock(id=wf_id)
|
|
465
|
+
|
|
466
|
+
def get_handle(wf_id):
|
|
467
|
+
handle = MagicMock()
|
|
468
|
+
if wf_id == "trigger-listener-wf-1-wh-1":
|
|
469
|
+
|
|
470
|
+
async def boom():
|
|
471
|
+
raise RuntimeError("simulated handle.cancel failure")
|
|
472
|
+
|
|
473
|
+
handle.cancel = boom
|
|
474
|
+
else:
|
|
475
|
+
|
|
476
|
+
async def ok():
|
|
477
|
+
cancelled_ids.append(wf_id)
|
|
478
|
+
|
|
479
|
+
handle.cancel = ok
|
|
480
|
+
return handle
|
|
481
|
+
|
|
482
|
+
client = MagicMock()
|
|
483
|
+
client.list_workflows = list_two
|
|
484
|
+
client.get_workflow_handle = get_handle
|
|
485
|
+
wrapper = MagicMock()
|
|
486
|
+
wrapper.client = client
|
|
487
|
+
|
|
488
|
+
from core import container as container_mod
|
|
489
|
+
|
|
490
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
491
|
+
|
|
492
|
+
cancelled = await mgr._cancel_canary_listeners("wf-1")
|
|
493
|
+
# Only the second one succeeded.
|
|
494
|
+
assert cancelled == 1
|
|
495
|
+
assert cancelled_ids == ["trigger-listener-wf-1-wh-2"]
|
|
496
|
+
|
|
497
|
+
@pytest.mark.asyncio
|
|
498
|
+
async def test_returns_zero_when_temporal_disconnected(self, monkeypatch):
|
|
499
|
+
mgr, _ = _build_manager_with_state("wf-1", nodes=[], edges=[])
|
|
500
|
+
|
|
501
|
+
wrapper = MagicMock()
|
|
502
|
+
wrapper.client = None
|
|
503
|
+
|
|
504
|
+
from core import container as container_mod
|
|
505
|
+
|
|
506
|
+
monkeypatch.setattr(container_mod.container, "temporal_client", lambda: wrapper)
|
|
507
|
+
|
|
508
|
+
cancelled = await mgr._cancel_canary_listeners("wf-1")
|
|
509
|
+
assert cancelled == 0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
# C1d.5 — invariant: DeploymentManager has NO instance state for canary listeners
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class TestCancelSweepsStuckNodeStatuses:
|
|
518
|
+
"""Regression: ``DeploymentManager.cancel`` must broadcast a full
|
|
519
|
+
status cleanup so the FE doesn't leave downstream nodes glowing
|
|
520
|
+
forever after the deployment goes down.
|
|
521
|
+
|
|
522
|
+
Pre-fix the cancel only reset trigger nodes (cron + listener) to
|
|
523
|
+
"idle". Downstream agents/tools/actions that were mid-execute when
|
|
524
|
+
cancel hit stayed in "executing" status on every connected client
|
|
525
|
+
because no sweep was issued. Also the toolbar Start/Stop indicator
|
|
526
|
+
stuck at ``executing=True`` because no terminal
|
|
527
|
+
``workflow_run_ended`` / ``update_workflow_status(executing=False)``
|
|
528
|
+
was emitted.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
def test_cancel_source_calls_stuck_node_sweep_with_include_waiting(self):
|
|
532
|
+
"""Source-introspection: ``cancel`` must call
|
|
533
|
+
``_clear_stuck_node_statuses(..., include_waiting=True)`` so
|
|
534
|
+
every node currently broadcast as ``executing`` OR ``waiting``
|
|
535
|
+
for this deployment goes back to idle on the FE. Without
|
|
536
|
+
``include_waiting=True`` non-firing trigger siblings (and any
|
|
537
|
+
other ``waiting`` indicators outside the cron/listener buckets
|
|
538
|
+
the manager owns directly) stay glowing."""
|
|
539
|
+
import inspect
|
|
540
|
+
|
|
541
|
+
from services.deployment.manager import DeploymentManager
|
|
542
|
+
|
|
543
|
+
src = inspect.getsource(DeploymentManager.cancel)
|
|
544
|
+
assert "_clear_stuck_node_statuses" in src, (
|
|
545
|
+
"DeploymentManager.cancel no longer sweeps stuck node "
|
|
546
|
+
"statuses. Downstream nodes mid-execute at cancel-time "
|
|
547
|
+
"will stay glowing on FE forever. Restore the "
|
|
548
|
+
"``_broadcaster._clear_stuck_node_statuses(workflow_id, "
|
|
549
|
+
"include_waiting=True)`` call after the trigger resets."
|
|
550
|
+
)
|
|
551
|
+
assert "include_waiting=True" in src, (
|
|
552
|
+
"DeploymentManager.cancel must pass include_waiting=True to "
|
|
553
|
+
"the stuck-node sweep — explicit user-cancel is the "
|
|
554
|
+
"'every indicator goes quiet' signal. Without it, sibling "
|
|
555
|
+
"trigger nodes (or other waiting indicators) outlive the "
|
|
556
|
+
"deployment."
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def test_cancel_source_broadcasts_terminal_workflow_status(self):
|
|
560
|
+
"""Source-introspection: ``cancel`` must emit a final
|
|
561
|
+
``update_workflow_status(executing=False, workflow_id=...)`` so
|
|
562
|
+
the toolbar Start/Stop indicator reflects the cancel. The
|
|
563
|
+
run-counter eviction in ``workflow_run_ended`` can race against
|
|
564
|
+
in-flight child runs that already incremented the counter."""
|
|
565
|
+
import inspect
|
|
566
|
+
|
|
567
|
+
from services.deployment.manager import DeploymentManager
|
|
568
|
+
|
|
569
|
+
src = inspect.getsource(DeploymentManager.cancel)
|
|
570
|
+
assert "update_workflow_status" in src, (
|
|
571
|
+
"DeploymentManager.cancel must broadcast "
|
|
572
|
+
"``update_workflow_status(executing=False, workflow_id=...)`` "
|
|
573
|
+
"so the FE toolbar Start/Stop indicator goes quiet after a "
|
|
574
|
+
"deployment is cancelled."
|
|
575
|
+
)
|
|
576
|
+
assert "executing=False" in src, (
|
|
577
|
+
"DeploymentManager.cancel must pass executing=False to the "
|
|
578
|
+
"terminal workflow_status broadcast. Anything else leaves "
|
|
579
|
+
"the toolbar showing the deployment as still active."
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
@pytest.mark.asyncio
|
|
583
|
+
async def test_cancel_runtime_calls_sweep_and_terminal_broadcast(self):
|
|
584
|
+
"""Runtime smoke: stub the broadcaster + state + trigger_manager
|
|
585
|
+
and assert cancel actually invokes the sweep + terminal status
|
|
586
|
+
broadcast with the right args."""
|
|
587
|
+
from services.deployment.manager import DeploymentManager
|
|
588
|
+
from services.deployment.state import DeploymentState
|
|
589
|
+
|
|
590
|
+
sweep_calls: List[Dict[str, Any]] = []
|
|
591
|
+
status_calls: List[Dict[str, Any]] = []
|
|
592
|
+
|
|
593
|
+
broadcaster = MagicMock()
|
|
594
|
+
broadcaster.update_node_status = AsyncMock()
|
|
595
|
+
broadcaster.update_workflow_status = AsyncMock(
|
|
596
|
+
side_effect=lambda **kw: status_calls.append(kw),
|
|
597
|
+
)
|
|
598
|
+
broadcaster._clear_stuck_node_statuses = AsyncMock(
|
|
599
|
+
side_effect=lambda workflow_id, include_waiting=False: sweep_calls.append(
|
|
600
|
+
{
|
|
601
|
+
"workflow_id": workflow_id,
|
|
602
|
+
"include_waiting": include_waiting,
|
|
603
|
+
}
|
|
604
|
+
)
|
|
605
|
+
or 0,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
database = MagicMock()
|
|
609
|
+
mgr = DeploymentManager(
|
|
610
|
+
database=database,
|
|
611
|
+
execute_workflow_fn=AsyncMock(return_value={"success": True}),
|
|
612
|
+
store_output_fn=AsyncMock(),
|
|
613
|
+
broadcaster=broadcaster,
|
|
614
|
+
)
|
|
615
|
+
# Seed deployment state — the cancel path bails early if
|
|
616
|
+
# the workflow isn't in self._deployments.
|
|
617
|
+
mgr._deployments["wf-1"] = DeploymentState(
|
|
618
|
+
deployment_id="deploy_wf-1",
|
|
619
|
+
workflow_id="wf-1",
|
|
620
|
+
is_running=True,
|
|
621
|
+
nodes=[{"id": "n1", "type": "aiAgent"}],
|
|
622
|
+
edges=[],
|
|
623
|
+
session_id="sess",
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Patch _cancel_canary_listeners / _cancel_canary_cron_schedules
|
|
627
|
+
# to be no-ops so the test focuses on the sweep + terminal
|
|
628
|
+
# broadcast contract.
|
|
629
|
+
mgr._cancel_canary_listeners = AsyncMock(return_value=0)
|
|
630
|
+
mgr._cancel_canary_cron_schedules = AsyncMock(return_value=0)
|
|
631
|
+
|
|
632
|
+
result = await mgr.cancel("wf-1")
|
|
633
|
+
assert result["success"] is True
|
|
634
|
+
|
|
635
|
+
# Sweep ran once with include_waiting=True (every indicator
|
|
636
|
+
# goes quiet on explicit user cancel).
|
|
637
|
+
assert len(sweep_calls) == 1, f"Expected one stuck-node sweep on cancel, got " f"{len(sweep_calls)}: {sweep_calls!r}"
|
|
638
|
+
assert sweep_calls[0] == {
|
|
639
|
+
"workflow_id": "wf-1",
|
|
640
|
+
"include_waiting": True,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Terminal executing=False broadcast for the toolbar.
|
|
644
|
+
assert {"executing": False, "workflow_id": "wf-1"} in status_calls, (
|
|
645
|
+
f"Expected update_workflow_status(executing=False, " f"workflow_id='wf-1') on cancel; got {status_calls!r}"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class TestNoInstanceStateForCanaryListeners:
|
|
650
|
+
"""Lock the architectural decision: DeploymentManager does NOT keep
|
|
651
|
+
a Python dict of canary listener handles or ids. Temporal's Visibility
|
|
652
|
+
store IS the registry. Adding such state would defeat the entire
|
|
653
|
+
durability guarantee (the dict dies on FastAPI restart, same gap the
|
|
654
|
+
canary is meant to close)."""
|
|
655
|
+
|
|
656
|
+
def test_no_canary_listeners_attribute(self):
|
|
657
|
+
from services.deployment.manager import DeploymentManager
|
|
658
|
+
|
|
659
|
+
database = MagicMock()
|
|
660
|
+
broadcaster = MagicMock()
|
|
661
|
+
|
|
662
|
+
mgr = DeploymentManager(
|
|
663
|
+
database=database,
|
|
664
|
+
execute_workflow_fn=AsyncMock(),
|
|
665
|
+
store_output_fn=AsyncMock(),
|
|
666
|
+
broadcaster=broadcaster,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
forbidden_attrs = {
|
|
670
|
+
"_canary_listeners",
|
|
671
|
+
"_listener_handles",
|
|
672
|
+
"_temporal_handles",
|
|
673
|
+
"_active_listener_workflows",
|
|
674
|
+
}
|
|
675
|
+
present = {a for a in forbidden_attrs if hasattr(mgr, a)}
|
|
676
|
+
assert not present, (
|
|
677
|
+
f"DeploymentManager has tribal listener-tracking state: {present}. "
|
|
678
|
+
"Use Visibility API queries instead — Temporal's server IS the registry. "
|
|
679
|
+
"Otherwise FastAPI restart drops the dict and the canary's durability "
|
|
680
|
+
"guarantee evaporates."
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def test_helper_methods_are_static_or_stateless(self):
|
|
684
|
+
"""``_listener_workflow_id`` and ``_canary_listener_enabled_for``
|
|
685
|
+
are intentionally @staticmethod — they don't read instance state.
|
|
686
|
+
Helper code that needs to know the listener id reconstructs it
|
|
687
|
+
from (workflow_id, node_id) deterministically.
|
|
688
|
+
"""
|
|
689
|
+
from services.deployment.manager import DeploymentManager
|
|
690
|
+
|
|
691
|
+
# Both helpers can be called on the class without an instance.
|
|
692
|
+
assert DeploymentManager._listener_workflow_id("wf", "node") == "trigger-listener-wf-node"
|