machinaos 0.0.76 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""OpenAI Codex CLI agent — multi-instance via `AICliService`.
|
|
2
|
+
|
|
3
|
+
Sandbox-first companion to `claude_code_agent`. Each task gets its own
|
|
4
|
+
git worktree; sandbox is enforced by Codex itself, not by us.
|
|
5
|
+
|
|
6
|
+
Codex has no session/resume/budget/turns surface — `CodexTaskSpec`
|
|
7
|
+
exposes only `sandbox` + `ask_for_approval` as task-level overrides.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, List, Optional
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
|
|
19
|
+
from core.logging import get_logger
|
|
20
|
+
from services.plugin import ActionNode, NodeContext, Operation, TaskQueue
|
|
21
|
+
from services.plugin.edge_walker import collect_agent_connections
|
|
22
|
+
|
|
23
|
+
from ._handles import STD_AGENT_HINTS, std_agent_handles
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
from services.cli_agent import CodexTaskSpec # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CodexAgentParams(BaseModel):
|
|
32
|
+
"""Multi-task batch parameters for Codex."""
|
|
33
|
+
|
|
34
|
+
tasks: List[CodexTaskSpec] = Field(
|
|
35
|
+
default_factory=list,
|
|
36
|
+
description="List of Codex tasks to run in parallel (max 5 concurrent).",
|
|
37
|
+
json_schema_extra={"rows": 1},
|
|
38
|
+
)
|
|
39
|
+
prompt: str = Field(
|
|
40
|
+
default="",
|
|
41
|
+
description="Legacy: single-prompt fallback used only when "
|
|
42
|
+
"`tasks` is empty.",
|
|
43
|
+
json_schema_extra={"rows": 4, "placeholder": "Or use the tasks array..."},
|
|
44
|
+
)
|
|
45
|
+
model: str = Field(
|
|
46
|
+
default="gpt-5.2-codex",
|
|
47
|
+
description="Default model for tasks that don't override it.",
|
|
48
|
+
)
|
|
49
|
+
sandbox: str = Field(
|
|
50
|
+
default="workspace-write",
|
|
51
|
+
description="Default sandbox for tasks that don't override it. "
|
|
52
|
+
"One of: read-only | workspace-write | danger-full-access.",
|
|
53
|
+
)
|
|
54
|
+
ask_for_approval: str = Field(
|
|
55
|
+
default="never",
|
|
56
|
+
description="Default approval mode: untrusted | on-request | never.",
|
|
57
|
+
)
|
|
58
|
+
system_prompt: Optional[str] = Field(default=None, json_schema_extra={"rows": 3})
|
|
59
|
+
working_directory: Optional[str] = None
|
|
60
|
+
max_parallel: int = Field(default=5, ge=1, le=20)
|
|
61
|
+
allowed_credentials: List[str] = Field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="ignore")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CodexAgentOutput(BaseModel):
|
|
67
|
+
success: bool = True
|
|
68
|
+
n_tasks: int = 0
|
|
69
|
+
n_succeeded: int = 0
|
|
70
|
+
n_failed: int = 0
|
|
71
|
+
wall_clock_ms: int = 0
|
|
72
|
+
tasks: List[Any] = Field(default_factory=list)
|
|
73
|
+
provider: str = "codex"
|
|
74
|
+
timestamp: Optional[str] = None
|
|
75
|
+
# Legacy single-task convenience
|
|
76
|
+
response: Optional[str] = None
|
|
77
|
+
model_config = ConfigDict(extra="allow")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CodexAgentNode(ActionNode):
|
|
81
|
+
type = "codex_agent"
|
|
82
|
+
display_name = "Codex"
|
|
83
|
+
subtitle = "Sandboxed Coding"
|
|
84
|
+
group = ("agent",)
|
|
85
|
+
description = (
|
|
86
|
+
"Run N parallel OpenAI Codex CLI sessions. Sandbox enforced "
|
|
87
|
+
"by Codex itself; per-task git worktree isolation."
|
|
88
|
+
)
|
|
89
|
+
component_kind = "agent"
|
|
90
|
+
handles = std_agent_handles()
|
|
91
|
+
ui_hints = STD_AGENT_HINTS
|
|
92
|
+
annotations = {"destructive": True, "readonly": False, "open_world": True}
|
|
93
|
+
task_queue = TaskQueue.AI_HEAVY
|
|
94
|
+
|
|
95
|
+
Params = CodexAgentParams
|
|
96
|
+
Output = CodexAgentOutput
|
|
97
|
+
|
|
98
|
+
@Operation(
|
|
99
|
+
"execute",
|
|
100
|
+
cost={"service": "codex_agent", "action": "run", "count": 1},
|
|
101
|
+
)
|
|
102
|
+
async def execute_op(
|
|
103
|
+
self, ctx: NodeContext, params: CodexAgentParams,
|
|
104
|
+
) -> Any:
|
|
105
|
+
from services.cli_agent.service import get_ai_cli_service
|
|
106
|
+
from services.cli_agent.types import session_result_to_model
|
|
107
|
+
from services.plugin.deps import get_database
|
|
108
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
109
|
+
|
|
110
|
+
start_time = time.time()
|
|
111
|
+
broadcaster = get_status_broadcaster()
|
|
112
|
+
workflow_id = ctx.workflow_id
|
|
113
|
+
node_id = ctx.node_id
|
|
114
|
+
|
|
115
|
+
await broadcaster.update_node_status(
|
|
116
|
+
node_id, "executing",
|
|
117
|
+
{"message": "Starting Codex batch..."},
|
|
118
|
+
workflow_id=workflow_id,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
tasks = list(params.tasks)
|
|
122
|
+
if not tasks:
|
|
123
|
+
prompt = params.prompt or self._infer_prompt_from_inputs(ctx, node_id)
|
|
124
|
+
if not prompt:
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"codex_agent: provide either `tasks` or `prompt`"
|
|
127
|
+
)
|
|
128
|
+
tasks = [
|
|
129
|
+
CodexTaskSpec(
|
|
130
|
+
prompt=prompt,
|
|
131
|
+
model=params.model,
|
|
132
|
+
sandbox=params.sandbox, # type: ignore[arg-type]
|
|
133
|
+
ask_for_approval=params.ask_for_approval, # type: ignore[arg-type]
|
|
134
|
+
system_prompt=params.system_prompt,
|
|
135
|
+
),
|
|
136
|
+
]
|
|
137
|
+
else:
|
|
138
|
+
for i, t in enumerate(tasks):
|
|
139
|
+
changed: dict = {}
|
|
140
|
+
if not t.model and params.model:
|
|
141
|
+
changed["model"] = params.model
|
|
142
|
+
if not t.system_prompt and params.system_prompt:
|
|
143
|
+
changed["system_prompt"] = params.system_prompt
|
|
144
|
+
if changed:
|
|
145
|
+
tasks[i] = t.model_copy(update=changed)
|
|
146
|
+
|
|
147
|
+
database = get_database()
|
|
148
|
+
_, skill_data, _, _, _ = await collect_agent_connections(
|
|
149
|
+
node_id, ctx.raw, database,
|
|
150
|
+
)
|
|
151
|
+
connected_skills = [
|
|
152
|
+
s.get("skill_name") or s.get("label")
|
|
153
|
+
for s in skill_data
|
|
154
|
+
if s.get("skill_name") or s.get("label")
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
workspace_dir = ctx.raw.get("workspace_dir") or params.working_directory
|
|
158
|
+
if workspace_dir is None:
|
|
159
|
+
from core.config import Settings
|
|
160
|
+
workspace_dir = Path(Settings().workspace_base_resolved) / (
|
|
161
|
+
workflow_id or "default"
|
|
162
|
+
)
|
|
163
|
+
workspace_dir = Path(workspace_dir)
|
|
164
|
+
|
|
165
|
+
repo_root = (
|
|
166
|
+
Path(params.working_directory) if params.working_directory else None
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
svc = get_ai_cli_service()
|
|
170
|
+
result = await svc.run_batch(
|
|
171
|
+
"codex",
|
|
172
|
+
tasks=tasks,
|
|
173
|
+
node_id=node_id,
|
|
174
|
+
workflow_id=workflow_id or "",
|
|
175
|
+
workspace_dir=workspace_dir,
|
|
176
|
+
broadcaster=broadcaster,
|
|
177
|
+
repo_root=repo_root,
|
|
178
|
+
connected_skill_names=connected_skills,
|
|
179
|
+
allowed_credentials=params.allowed_credentials,
|
|
180
|
+
max_parallel=params.max_parallel,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
elapsed = time.time() - start_time
|
|
184
|
+
logger.debug(
|
|
185
|
+
"[codex_agent] node=%s tasks=%d ok=%d fail=%d elapsed=%.2fs",
|
|
186
|
+
node_id, result.n_tasks, result.n_succeeded, result.n_failed, elapsed,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
await broadcaster.update_node_status(
|
|
190
|
+
node_id, "success" if result.n_failed == 0 else "warning",
|
|
191
|
+
{
|
|
192
|
+
"message": (
|
|
193
|
+
f"Batch complete: {result.n_succeeded}/{result.n_tasks} "
|
|
194
|
+
f"succeeded"
|
|
195
|
+
),
|
|
196
|
+
"n_tasks": result.n_tasks,
|
|
197
|
+
"n_succeeded": result.n_succeeded,
|
|
198
|
+
"n_failed": result.n_failed,
|
|
199
|
+
},
|
|
200
|
+
workflow_id=workflow_id,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
task_models = [session_result_to_model(t).model_dump() for t in result.tasks]
|
|
204
|
+
|
|
205
|
+
legacy_response = (
|
|
206
|
+
result.tasks[0].response if len(result.tasks) == 1 else None
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"success": result.n_failed == 0,
|
|
211
|
+
"n_tasks": result.n_tasks,
|
|
212
|
+
"n_succeeded": result.n_succeeded,
|
|
213
|
+
"n_failed": result.n_failed,
|
|
214
|
+
"total_cost_usd": result.total_cost_usd, # always None for Codex
|
|
215
|
+
"wall_clock_ms": result.wall_clock_ms,
|
|
216
|
+
"tasks": task_models,
|
|
217
|
+
"provider": result.provider,
|
|
218
|
+
"timestamp": result.timestamp or datetime.now().isoformat(),
|
|
219
|
+
"response": legacy_response,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _infer_prompt_from_inputs(ctx: NodeContext, node_id: str) -> str:
|
|
224
|
+
for edge in ctx.raw.get("edges", []):
|
|
225
|
+
if edge.get("target") != node_id:
|
|
226
|
+
continue
|
|
227
|
+
handle = edge.get("targetHandle")
|
|
228
|
+
if handle not in ("input-main", None):
|
|
229
|
+
continue
|
|
230
|
+
src = ctx.raw.get("outputs", {}).get(edge.get("source"), {})
|
|
231
|
+
if isinstance(src, dict):
|
|
232
|
+
for k in ("message", "text", "content", "prompt"):
|
|
233
|
+
val = src.get(k)
|
|
234
|
+
if val:
|
|
235
|
+
return str(val)
|
|
236
|
+
return str(src)
|
|
237
|
+
elif src:
|
|
238
|
+
return str(src)
|
|
239
|
+
return ""
|
|
@@ -39,11 +39,11 @@ class DeepAgentNode(ActionNode):
|
|
|
39
39
|
|
|
40
40
|
@Operation("execute", cost={"service": "deep_agent", "action": "run", "count": 1})
|
|
41
41
|
async def execute_op(self, ctx: NodeContext, params: SpecializedAgentParams) -> Any:
|
|
42
|
-
from
|
|
42
|
+
from services.plugin.deps import get_ai_service, get_database
|
|
43
43
|
from services.status_broadcaster import get_status_broadcaster
|
|
44
44
|
|
|
45
|
-
ai_service =
|
|
46
|
-
database =
|
|
45
|
+
ai_service = get_ai_service()
|
|
46
|
+
database = get_database()
|
|
47
47
|
node_id = ctx.node_id
|
|
48
48
|
workflow_id = ctx.workflow_id
|
|
49
49
|
payload = params.model_dump()
|
|
@@ -38,11 +38,11 @@ class RLMAgentNode(ActionNode):
|
|
|
38
38
|
|
|
39
39
|
@Operation("execute", cost={"service": "rlm_agent", "action": "run", "count": 1})
|
|
40
40
|
async def execute_op(self, ctx: NodeContext, params: SpecializedAgentParams) -> Any:
|
|
41
|
-
from
|
|
41
|
+
from services.plugin.deps import get_ai_service, get_database
|
|
42
42
|
from services.status_broadcaster import get_status_broadcaster
|
|
43
43
|
|
|
44
|
-
ai_service =
|
|
45
|
-
database =
|
|
44
|
+
ai_service = get_ai_service()
|
|
45
|
+
database = get_database()
|
|
46
46
|
node_id = ctx.node_id
|
|
47
47
|
workflow_id = ctx.workflow_id
|
|
48
48
|
payload = params.model_dump()
|
|
@@ -1 +1,31 @@
|
|
|
1
|
-
"""Plugins for the 'android' palette group.
|
|
1
|
+
"""Plugins for the 'android' palette group.
|
|
2
|
+
|
|
3
|
+
Self-contained plugin folder (Wave 11.H pattern) — owns its
|
|
4
|
+
WebSocket handlers (`_handlers.py`), HTTP router (`_router.py`),
|
|
5
|
+
action-dispatch service (`_dispatcher.py`), and the relay-pairing
|
|
6
|
+
sub-package (`_relay/`). The 16 service plugins (battery_monitor,
|
|
7
|
+
wifi_automation, ...) live alongside as siblings.
|
|
8
|
+
|
|
9
|
+
Wiring is body-free; both registrations are idempotent.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from services.status_broadcaster import register_service_refresh
|
|
13
|
+
from services.ws_handler_registry import (
|
|
14
|
+
register_option_loader,
|
|
15
|
+
register_router,
|
|
16
|
+
register_ws_handlers,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from . import _router
|
|
20
|
+
from ._handlers import WS_HANDLERS
|
|
21
|
+
from ._option_loaders import load_service_actions
|
|
22
|
+
from ._refresh import refresh_android_status
|
|
23
|
+
|
|
24
|
+
register_ws_handlers(WS_HANDLERS)
|
|
25
|
+
register_router(_router.router, name="android")
|
|
26
|
+
register_service_refresh(refresh_android_status)
|
|
27
|
+
|
|
28
|
+
# loadOptionsMethod loader (Wave 11.I, milestone M.3) -- last entry to
|
|
29
|
+
# leave services/node_option_loaders/, which is deleted at the same
|
|
30
|
+
# commit.
|
|
31
|
+
register_option_loader("getAndroidServiceActions", load_service_actions)
|
|
@@ -90,9 +90,13 @@ class AndroidServiceOutput(BaseModel):
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
class AndroidServiceBase(ActionNode, abstract=True):
|
|
93
|
-
"""Subclass and set type/display_name/icon/description.
|
|
93
|
+
"""Subclass and set type/display_name/icon/description.
|
|
94
|
+
|
|
95
|
+
Visual metadata (icon + color) lives in ``server/nodes/visuals.json``
|
|
96
|
+
keyed by individual plugin type. The ``_visuals.py`` resolver picks
|
|
97
|
+
each entry up at NodeSpec emit time; no class-level ClassVars needed.
|
|
98
|
+
"""
|
|
94
99
|
|
|
95
|
-
color = "#50fa7b"
|
|
96
100
|
group = ("android", "service")
|
|
97
101
|
component_kind = "square"
|
|
98
102
|
handles = (
|
|
@@ -110,9 +114,9 @@ class AndroidServiceBase(ActionNode, abstract=True):
|
|
|
110
114
|
|
|
111
115
|
@Operation("invoke", cost={"service": "android", "action": "service_call", "count": 1})
|
|
112
116
|
async def invoke(self, ctx: NodeContext, params: AndroidServiceParams) -> Any:
|
|
113
|
-
from
|
|
117
|
+
from services.plugin.deps import get_android_service
|
|
114
118
|
|
|
115
|
-
android_service =
|
|
119
|
+
android_service = get_android_service()
|
|
116
120
|
payload = params.model_dump()
|
|
117
121
|
|
|
118
122
|
# Derive service_id from the registered node type (battery etc.) —
|
|
@@ -174,7 +178,7 @@ async def _execute_with_broadcast(
|
|
|
174
178
|
host: str, port: int, log_label: str,
|
|
175
179
|
) -> Dict[str, Any]:
|
|
176
180
|
"""Run an Android service call with broadcast status for the UI node."""
|
|
177
|
-
from
|
|
181
|
+
from ._dispatcher import AndroidService
|
|
178
182
|
from services.status_broadcaster import get_status_broadcaster
|
|
179
183
|
|
|
180
184
|
broadcaster = get_status_broadcaster()
|
|
@@ -218,7 +218,7 @@ class AndroidService:
|
|
|
218
218
|
Execution result with service response data
|
|
219
219
|
"""
|
|
220
220
|
try:
|
|
221
|
-
from
|
|
221
|
+
from ._relay import get_current_relay_client
|
|
222
222
|
|
|
223
223
|
relay_client = get_current_relay_client()
|
|
224
224
|
if not relay_client:
|
|
@@ -373,7 +373,7 @@ class AndroidService:
|
|
|
373
373
|
|
|
374
374
|
try:
|
|
375
375
|
# Check if relay connection is available (remote device)
|
|
376
|
-
from
|
|
376
|
+
from ._relay import get_current_relay_client
|
|
377
377
|
relay_client = get_current_relay_client()
|
|
378
378
|
|
|
379
379
|
if relay_client and relay_client.is_paired():
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""WebSocket handlers for the Android plugin.
|
|
2
|
+
|
|
3
|
+
Five handlers split into two concerns:
|
|
4
|
+
|
|
5
|
+
- ADB / device-action commands (`get_android_devices`,
|
|
6
|
+
`execute_android_action`) talk to the local `AndroidService`
|
|
7
|
+
dispatcher to enumerate devices and run service actions.
|
|
8
|
+
|
|
9
|
+
- Relay-pairing commands (`android_relay_connect` / `_disconnect` /
|
|
10
|
+
`_reconnect`) drive the WebSocket relay client lifecycle for
|
|
11
|
+
remote devices.
|
|
12
|
+
|
|
13
|
+
Self-registers via ``services.ws_handler_registry.register_ws_handlers``
|
|
14
|
+
from ``__init__.py`` (Wave 11.H plugin pattern).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
from starlette.websockets import WebSocket
|
|
24
|
+
|
|
25
|
+
from core.logging import get_logger
|
|
26
|
+
from services.plugin.ws import ws_response
|
|
27
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def handle_get_android_devices(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
33
|
+
"""Get list of connected Android devices."""
|
|
34
|
+
from services.plugin.deps import get_android_service
|
|
35
|
+
|
|
36
|
+
android_service = get_android_service()
|
|
37
|
+
devices = await android_service.list_devices()
|
|
38
|
+
return {"devices": devices, "timestamp": time.time()}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def handle_execute_android_action(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
42
|
+
"""Execute an Android service action."""
|
|
43
|
+
from services.plugin.deps import get_android_service
|
|
44
|
+
|
|
45
|
+
android_service = get_android_service()
|
|
46
|
+
broadcaster = get_status_broadcaster()
|
|
47
|
+
service_id, action = data["service_id"], data["action"]
|
|
48
|
+
node_id = data.get("node_id", f"android_{service_id}_{action}")
|
|
49
|
+
|
|
50
|
+
await broadcaster.update_node_status(node_id, "executing")
|
|
51
|
+
result = await android_service.execute_service(
|
|
52
|
+
node_id=node_id, service_id=service_id, action=action,
|
|
53
|
+
parameters=data.get("parameters", {}),
|
|
54
|
+
android_host=data.get("android_host", "localhost"),
|
|
55
|
+
android_port=data.get("android_port", 8888)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
status = "success" if result.get("success") else "error"
|
|
59
|
+
await broadcaster.update_node_status(
|
|
60
|
+
node_id, status, result.get("result") or {"error": result.get("error")}
|
|
61
|
+
)
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@ws_response
|
|
66
|
+
async def handle_android_relay_connect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
67
|
+
"""Connect to the Android relay server.
|
|
68
|
+
|
|
69
|
+
Establishes the WebSocket connection and broadcasts QR pairing data.
|
|
70
|
+
Status updates are emitted from the relay client itself.
|
|
71
|
+
"""
|
|
72
|
+
from ._relay import get_relay_client
|
|
73
|
+
|
|
74
|
+
url = data.get("url", "")
|
|
75
|
+
api_key = data.get("api_key")
|
|
76
|
+
|
|
77
|
+
if not url:
|
|
78
|
+
return {"success": False, "connected": False, "error": "Relay URL is required"}
|
|
79
|
+
if not api_key:
|
|
80
|
+
return {"success": False, "connected": False, "error": "API key is required"}
|
|
81
|
+
|
|
82
|
+
logger.info(f"[WebSocket] Android relay connect: {url}")
|
|
83
|
+
|
|
84
|
+
client, error = await get_relay_client(url, api_key)
|
|
85
|
+
if client:
|
|
86
|
+
logger.info(
|
|
87
|
+
f"[WebSocket] Android relay connect success, qr_data present: "
|
|
88
|
+
f"{bool(client.qr_data)}, session_token: {client.session_token}"
|
|
89
|
+
)
|
|
90
|
+
return {
|
|
91
|
+
"success": True,
|
|
92
|
+
"connected": True,
|
|
93
|
+
"session_token": client.session_token,
|
|
94
|
+
"qr_data": client.qr_data,
|
|
95
|
+
"message": "Connected to relay server",
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
"success": False,
|
|
99
|
+
"connected": False,
|
|
100
|
+
"error": error or "Failed to connect to relay server",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@ws_response
|
|
105
|
+
async def handle_android_relay_disconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
106
|
+
"""Disconnect from the Android relay server."""
|
|
107
|
+
from ._relay import close_relay_client
|
|
108
|
+
|
|
109
|
+
logger.info("[WebSocket] Android relay disconnect requested")
|
|
110
|
+
await close_relay_client()
|
|
111
|
+
return {"success": True, "connected": False, "message": "Disconnected from relay server"}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@ws_response
|
|
115
|
+
async def handle_android_relay_reconnect(data: Dict[str, Any], websocket: WebSocket) -> Dict[str, Any]:
|
|
116
|
+
"""Reconnect to the relay with a fresh session token + QR code."""
|
|
117
|
+
from ._relay import close_relay_client, get_relay_client
|
|
118
|
+
|
|
119
|
+
url = data.get("url", "")
|
|
120
|
+
api_key = data.get("api_key")
|
|
121
|
+
|
|
122
|
+
if not url:
|
|
123
|
+
return {"success": False, "connected": False, "error": "Relay URL is required"}
|
|
124
|
+
if not api_key:
|
|
125
|
+
return {"success": False, "connected": False, "error": "API key is required"}
|
|
126
|
+
|
|
127
|
+
logger.info("[WebSocket] Android relay reconnect: forcing new session")
|
|
128
|
+
|
|
129
|
+
await close_relay_client()
|
|
130
|
+
await asyncio.sleep(0.5) # ensure clean disconnect
|
|
131
|
+
|
|
132
|
+
client, error = await get_relay_client(url, api_key)
|
|
133
|
+
if client:
|
|
134
|
+
return {
|
|
135
|
+
"success": True,
|
|
136
|
+
"connected": True,
|
|
137
|
+
"session_token": client.session_token,
|
|
138
|
+
"qr_data": client.qr_data,
|
|
139
|
+
"message": "Reconnected with new session token",
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
"success": False,
|
|
143
|
+
"connected": False,
|
|
144
|
+
"error": error or "Failed to reconnect to relay server",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
WS_HANDLERS = {
|
|
149
|
+
"get_android_devices": handle_get_android_devices,
|
|
150
|
+
"execute_android_action": handle_execute_android_action,
|
|
151
|
+
"android_relay_connect": handle_android_relay_connect,
|
|
152
|
+
"android_relay_disconnect": handle_android_relay_disconnect,
|
|
153
|
+
"android_relay_reconnect": handle_android_relay_reconnect,
|
|
154
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Android ``loadOptionsMethod`` loaders.
|
|
2
|
+
|
|
3
|
+
Wave 11.I, milestone M.3. Returns the list of actions supported by each
|
|
4
|
+
Android service node. The frontend passes ``node_type`` in params; the
|
|
5
|
+
loader maps it to the service's ``action`` enum advertised by the
|
|
6
|
+
running Android bridge.
|
|
7
|
+
|
|
8
|
+
``SERVICE_ID_MAP`` is the single source of truth -- imported from
|
|
9
|
+
``_base.py`` instead of being duplicated here (the pre-Wave-11.I
|
|
10
|
+
``services/node_option_loaders/android_loaders.py`` carried its own
|
|
11
|
+
copy under a different name; that copy is gone now).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, List
|
|
17
|
+
|
|
18
|
+
from ._base import SERVICE_ID_MAP
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def load_service_actions(params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
22
|
+
"""Return ``[{value, label}]`` for the given Android service node.
|
|
23
|
+
|
|
24
|
+
Each service's ``execute_action`` endpoint validates the action at
|
|
25
|
+
call time; the dropdown is convenience UX, not a correctness
|
|
26
|
+
boundary. The frontend falls back to a free-text input if the
|
|
27
|
+
loader returns an empty list (service offline / discovery failed).
|
|
28
|
+
"""
|
|
29
|
+
node_type = params.get("node_type") or ""
|
|
30
|
+
service_id = SERVICE_ID_MAP.get(node_type)
|
|
31
|
+
if not service_id:
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from services.plugin.deps import get_android_service
|
|
36
|
+
|
|
37
|
+
android_svc = get_android_service()
|
|
38
|
+
actions = await android_svc.list_actions(service_id) # type: ignore[attr-defined]
|
|
39
|
+
return [
|
|
40
|
+
{"value": a, "label": a.replace("_", " ").title()}
|
|
41
|
+
for a in actions or []
|
|
42
|
+
]
|
|
43
|
+
except Exception:
|
|
44
|
+
return []
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Android service-status refresh callback (Wave 11.I, milestone J).
|
|
2
|
+
|
|
3
|
+
Moved from ``services/status_broadcaster._auto_reconnect_android_relay``
|
|
4
|
+
(plus the ``_auto_reconnect_android_relay_body`` helper). Plugin
|
|
5
|
+
packages register their own callback via
|
|
6
|
+
``status_broadcaster.register_service_refresh``; the broadcaster no
|
|
7
|
+
longer hardcodes a per-service refresh.
|
|
8
|
+
|
|
9
|
+
Load-bearing: this is the path that auto-reconnects the Android relay
|
|
10
|
+
from a stored pairing session after server restart. Without it the
|
|
11
|
+
relay sits idle until the user manually clicks Connect. Runs once on
|
|
12
|
+
lifespan startup (post-V).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from opentelemetry import trace
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from services.status_broadcaster import StatusBroadcaster
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
tracer = trace.get_tracer(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def refresh_android_status(broadcaster: "StatusBroadcaster") -> None:
|
|
30
|
+
"""Auto-reconnect to the Android relay if a stored pairing exists.
|
|
31
|
+
|
|
32
|
+
Called once per ``_refresh_all_services`` cycle (post-V: at
|
|
33
|
+
lifespan startup only).
|
|
34
|
+
"""
|
|
35
|
+
with tracer.start_as_current_span("broadcaster.refresh_android") as span:
|
|
36
|
+
await _auto_reconnect_body(broadcaster, span)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _auto_reconnect_body(broadcaster: "StatusBroadcaster", span) -> None:
|
|
40
|
+
try:
|
|
41
|
+
# Already connected? Refresh the cached snapshot and stop.
|
|
42
|
+
from ._relay.manager import get_current_relay_client
|
|
43
|
+
|
|
44
|
+
existing = get_current_relay_client()
|
|
45
|
+
if existing and existing.is_connected():
|
|
46
|
+
broadcaster._status["android"] = {
|
|
47
|
+
"connected": True,
|
|
48
|
+
"paired": existing.is_paired(),
|
|
49
|
+
"device_id": existing.paired_device_id,
|
|
50
|
+
"device_name": existing.paired_device_name,
|
|
51
|
+
"connected_devices": list(existing.get_connected_devices()),
|
|
52
|
+
"connection_type": "relay",
|
|
53
|
+
"qr_data": existing.qr_data,
|
|
54
|
+
"session_token": existing.session_token,
|
|
55
|
+
}
|
|
56
|
+
logger.debug("[StatusBroadcaster] Android relay already connected")
|
|
57
|
+
await broadcaster.broadcast({
|
|
58
|
+
"type": "android_status",
|
|
59
|
+
"data": broadcaster._status["android"],
|
|
60
|
+
})
|
|
61
|
+
span.set_attribute("path", "already_connected")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Look for a stored pairing session.
|
|
65
|
+
from services.plugin.deps import get_database
|
|
66
|
+
|
|
67
|
+
database = get_database()
|
|
68
|
+
session = await database.get_android_relay_session()
|
|
69
|
+
if not session:
|
|
70
|
+
span.set_attribute("path", "no_session")
|
|
71
|
+
logger.debug("[StatusBroadcaster] No stored Android relay session")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
relay_url = session.get("relay_url")
|
|
75
|
+
api_key = session.get("api_key")
|
|
76
|
+
device_id = session.get("device_id")
|
|
77
|
+
|
|
78
|
+
if not relay_url or not api_key:
|
|
79
|
+
span.set_attribute("path", "session_missing_creds")
|
|
80
|
+
logger.debug(
|
|
81
|
+
"[StatusBroadcaster] Stored session missing relay URL or API key"
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
span.set_attribute("path", "auto_reconnect")
|
|
86
|
+
logger.info(
|
|
87
|
+
"[StatusBroadcaster] Auto-reconnecting to Android relay...",
|
|
88
|
+
relay_url=relay_url,
|
|
89
|
+
device_id=device_id,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
from ._relay.manager import get_relay_client
|
|
93
|
+
|
|
94
|
+
client, error = await get_relay_client(relay_url, api_key)
|
|
95
|
+
|
|
96
|
+
if client and client.is_connected():
|
|
97
|
+
logger.info("[StatusBroadcaster] Android relay reconnected successfully")
|
|
98
|
+
# The relay server creates a new session on each connect, so
|
|
99
|
+
# pairing may be lost -- mirror whatever the new client
|
|
100
|
+
# reports.
|
|
101
|
+
broadcaster._status["android"] = {
|
|
102
|
+
"connected": True,
|
|
103
|
+
"paired": client.is_paired(),
|
|
104
|
+
"device_id": client.paired_device_id,
|
|
105
|
+
"device_name": client.paired_device_name,
|
|
106
|
+
"connected_devices": list(client.get_connected_devices()),
|
|
107
|
+
"connection_type": "relay",
|
|
108
|
+
"qr_data": client.qr_data,
|
|
109
|
+
"session_token": client.session_token,
|
|
110
|
+
}
|
|
111
|
+
await broadcaster.broadcast({
|
|
112
|
+
"type": "android_status",
|
|
113
|
+
"data": broadcaster._status["android"],
|
|
114
|
+
})
|
|
115
|
+
span.set_attribute("reconnect_ok", True)
|
|
116
|
+
else:
|
|
117
|
+
span.set_attribute("reconnect_ok", False)
|
|
118
|
+
logger.warning(
|
|
119
|
+
"[StatusBroadcaster] Failed to reconnect Android relay: %s", error
|
|
120
|
+
)
|
|
121
|
+
# Stored session is stale; drop it.
|
|
122
|
+
await database.clear_android_relay_session()
|
|
123
|
+
except Exception as exc: # noqa: BLE001 -- mirror pre-migration behaviour
|
|
124
|
+
span.record_exception(exc)
|
|
125
|
+
logger.debug(
|
|
126
|
+
"[StatusBroadcaster] Could not auto-reconnect Android relay: %s", exc
|
|
127
|
+
)
|