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,482 @@
|
|
|
1
|
+
"""MachinaOs MCP server (VSCode pattern, no custom IPC).
|
|
2
|
+
|
|
3
|
+
Hosts a `FastMCP` ASGI sub-app at ``/mcp/ide`` that spawned CLI sessions
|
|
4
|
+
auto-discover via the lockfile written by ``lockfile.py``. Each session
|
|
5
|
+
gets a per-batch bearer token; the middleware validates it and binds the
|
|
6
|
+
matching ``BatchContext`` into a contextvar so tool implementations can
|
|
7
|
+
scope to the calling session's workspace_dir / connected_skill_names /
|
|
8
|
+
allowed_credentials without explicit plumbing.
|
|
9
|
+
|
|
10
|
+
Tools (5 in v1, mirroring Claude Code's progressive-disclosure pattern):
|
|
11
|
+
- ``getWorkspaceFiles`` — glob/read inside the session's worktree
|
|
12
|
+
- ``listSkills`` — metadata for skills connected to the parent agent
|
|
13
|
+
- ``getSkill`` — full skill markdown + scripts + references
|
|
14
|
+
- ``getCredential`` — gated by per-batch allowlist
|
|
15
|
+
- ``broadcastLog`` — write to MachinaOs Terminal tab
|
|
16
|
+
|
|
17
|
+
The server module exposes:
|
|
18
|
+
- :func:`get_mcp_app` — Starlette/ASGI sub-app for ``app.mount(...)``
|
|
19
|
+
- :func:`register_batch` / :func:`unregister_batch` — `AICliService`
|
|
20
|
+
calls these around `run_batch()` to register/expire tokens
|
|
21
|
+
- :class:`BatchContext` — scoping data attached to each token
|
|
22
|
+
|
|
23
|
+
Tools deferred to v2: ``getDiagnostics``, ``executeCode``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import contextvars
|
|
29
|
+
import logging
|
|
30
|
+
import secrets
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Set
|
|
34
|
+
|
|
35
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
36
|
+
from starlette.requests import Request
|
|
37
|
+
from starlette.responses import JSONResponse, Response
|
|
38
|
+
from starlette.routing import Mount
|
|
39
|
+
|
|
40
|
+
from core.logging import get_logger
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# BatchContext + token registry
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class BatchContext:
|
|
51
|
+
"""Scoping data attached to one batch's bearer token.
|
|
52
|
+
|
|
53
|
+
Populated at ``AICliService.run_batch()`` entry; deregistered in the
|
|
54
|
+
``finally`` block. Tools dereference the calling batch via the
|
|
55
|
+
bearer token in `Authorization` header.
|
|
56
|
+
"""
|
|
57
|
+
workflow_id: str
|
|
58
|
+
node_id: str
|
|
59
|
+
workspace_dir: Path
|
|
60
|
+
connected_skill_names: Set[str] = field(default_factory=set)
|
|
61
|
+
allowed_credentials: Set[str] = field(default_factory=set)
|
|
62
|
+
# Connected ``input-tools`` nodes (entries from
|
|
63
|
+
# ``services.plugin.edge_walker.collect_agent_connections``). Each
|
|
64
|
+
# dict: ``{node_id, node_type, label, parameters, ...}``. Drives the
|
|
65
|
+
# ``listMachinaOsTools`` / ``callMachinaOsTool`` MCP tools so the
|
|
66
|
+
# CLI agent sees the same tool surface the AI Agent does.
|
|
67
|
+
connected_tools: List[Dict[str, Any]] = field(default_factory=list)
|
|
68
|
+
# Optional broadcaster for `broadcastLog`. Lazily resolved from the
|
|
69
|
+
# global container if None.
|
|
70
|
+
broadcaster: Optional[Any] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Token -> BatchContext registry. Lives in-memory only; tokens never
|
|
74
|
+
# touch disk or the credentials.db.
|
|
75
|
+
_active_tokens: Dict[str, BatchContext] = {}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def issue_token() -> str:
|
|
79
|
+
"""Mint a new bearer token (32 bytes hex)."""
|
|
80
|
+
return secrets.token_hex(32)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_batch(token: str, ctx: BatchContext) -> None:
|
|
84
|
+
"""Register a batch's auth token + expose its connected workflow
|
|
85
|
+
tools on the FastMCP server. Idempotent on identical context."""
|
|
86
|
+
if token in _active_tokens:
|
|
87
|
+
existing = _active_tokens[token]
|
|
88
|
+
if existing is not ctx:
|
|
89
|
+
raise ValueError("Token collision in MCP server batch registry")
|
|
90
|
+
return
|
|
91
|
+
_active_tokens[token] = ctx
|
|
92
|
+
tool_names = [t.get("node_type") for t in ctx.connected_tools]
|
|
93
|
+
logger.info(
|
|
94
|
+
"[CC-Agent MCP register_batch] node=%s wf=%s token=%s... "
|
|
95
|
+
"skills=%d tools=%d creds=%d %s",
|
|
96
|
+
ctx.node_id, ctx.workflow_id, token[:8],
|
|
97
|
+
len(ctx.connected_skill_names), len(ctx.connected_tools),
|
|
98
|
+
len(ctx.allowed_credentials),
|
|
99
|
+
f"tools={tool_names}" if tool_names else "(no tools wired)",
|
|
100
|
+
)
|
|
101
|
+
# Per-batch workflow-tool exposure lives in workflow_tools.py.
|
|
102
|
+
from services.cli_agent.workflow_tools import expose_workflow_tools
|
|
103
|
+
expose_workflow_tools(ctx.connected_tools)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def unregister_batch(token: str) -> None:
|
|
107
|
+
"""Drop a batch's token + un-expose its tools when the refcount
|
|
108
|
+
hits zero. Safe to call twice."""
|
|
109
|
+
ctx = _active_tokens.pop(token, None)
|
|
110
|
+
if ctx is None:
|
|
111
|
+
return
|
|
112
|
+
logger.debug("[CC-Agent MCP] unregistered batch token=%s...", token[:8])
|
|
113
|
+
from services.cli_agent.workflow_tools import unexpose_workflow_tools
|
|
114
|
+
unexpose_workflow_tools(ctx.connected_tools)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def lookup_batch(token: str) -> Optional[BatchContext]:
|
|
118
|
+
return _active_tokens.get(token)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def active_batch_count() -> int:
|
|
122
|
+
return len(_active_tokens)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# ContextVar — thread/task-local handle to the current batch
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
_current_batch: contextvars.ContextVar[Optional[BatchContext]] = (
|
|
130
|
+
contextvars.ContextVar("machina_current_batch", default=None)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _require_batch() -> BatchContext:
|
|
135
|
+
ctx = _current_batch.get()
|
|
136
|
+
if ctx is None:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
"MCP tool called without an active batch context. "
|
|
139
|
+
"This indicates the auth middleware was bypassed."
|
|
140
|
+
)
|
|
141
|
+
return ctx
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Per-batch workflow-tool exposure happens in `_expose_workflow_tools`
|
|
145
|
+
# (above). Each connected node lands as its own
|
|
146
|
+
# `mcp__machinaos__<node_type>` entry; FastMCP infers the inputSchema
|
|
147
|
+
# from the typed `params` annotation. The legacy generic wrapper has
|
|
148
|
+
# been removed.
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Auth middleware
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
class _BearerAuthMiddleware(BaseHTTPMiddleware):
|
|
156
|
+
"""Validate `Authorization: Bearer <token>` against the registry.
|
|
157
|
+
|
|
158
|
+
On success: bind the matching `BatchContext` into the contextvar.
|
|
159
|
+
On failure: 401.
|
|
160
|
+
|
|
161
|
+
The MCP spec uses MCP-Protocol-Version + Authorization headers; we
|
|
162
|
+
care only about the Bearer here. Health checks under `/healthz`
|
|
163
|
+
bypass auth (they're for dev sanity).
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
async def dispatch( # type: ignore[override]
|
|
167
|
+
self,
|
|
168
|
+
request: Request,
|
|
169
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
170
|
+
) -> Response:
|
|
171
|
+
path = request.url.path
|
|
172
|
+
method = request.method
|
|
173
|
+
if path.endswith("/healthz"):
|
|
174
|
+
return await call_next(request)
|
|
175
|
+
|
|
176
|
+
auth = request.headers.get("authorization") or request.headers.get(
|
|
177
|
+
"Authorization"
|
|
178
|
+
)
|
|
179
|
+
token: Optional[str] = None
|
|
180
|
+
if auth and auth.lower().startswith("bearer "):
|
|
181
|
+
token = auth[7:].strip() or None
|
|
182
|
+
|
|
183
|
+
if not token:
|
|
184
|
+
ua = request.headers.get("user-agent", "")
|
|
185
|
+
logger.warning(
|
|
186
|
+
"[CC-Agent MCP auth] %s %s -> 401 (no Bearer token; "
|
|
187
|
+
"auth_header=%r ua=%r) — claude CLI either didn't read the "
|
|
188
|
+
"lockfile or is hitting the wrong URL.",
|
|
189
|
+
method, path, auth, ua,
|
|
190
|
+
)
|
|
191
|
+
return JSONResponse(
|
|
192
|
+
{"error": "missing or malformed Authorization header"},
|
|
193
|
+
status_code=401,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
ctx = lookup_batch(token)
|
|
197
|
+
if ctx is None:
|
|
198
|
+
logger.warning(
|
|
199
|
+
"[CC-Agent MCP auth] %s %s -> 401 (token=%s... not in "
|
|
200
|
+
"active registry — batch may have ended)",
|
|
201
|
+
method, path, token[:8],
|
|
202
|
+
)
|
|
203
|
+
return JSONResponse(
|
|
204
|
+
{"error": "invalid or expired token"},
|
|
205
|
+
status_code=401,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
logger.info(
|
|
209
|
+
"[CC-Agent MCP auth] %s %s -> OK (node=%s wf=%s token=%s...)",
|
|
210
|
+
method, path, ctx.node_id, ctx.workflow_id, token[:8],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
reset_token = _current_batch.set(ctx)
|
|
214
|
+
try:
|
|
215
|
+
return await call_next(request)
|
|
216
|
+
finally:
|
|
217
|
+
_current_batch.reset(reset_token)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Tool registration helper — defers FastMCP import so module import is cheap
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def _build_tools(mcp: Any) -> None: # FastMCP type
|
|
225
|
+
"""Register the 5 v1 tools on a `FastMCP` instance."""
|
|
226
|
+
|
|
227
|
+
@mcp.tool(
|
|
228
|
+
name="getWorkspaceFiles",
|
|
229
|
+
description=(
|
|
230
|
+
"List or read files inside the calling session's per-task git "
|
|
231
|
+
"worktree. Use `read=False` for metadata-only listings; "
|
|
232
|
+
"`read=True` to fetch file contents (capped at 1MB per file)."
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
def get_workspace_files(
|
|
236
|
+
path: str = ".",
|
|
237
|
+
pattern: str = "*",
|
|
238
|
+
read: bool = False,
|
|
239
|
+
max_bytes: int = 1_000_000,
|
|
240
|
+
) -> Dict[str, Any]:
|
|
241
|
+
ctx = _require_batch()
|
|
242
|
+
try:
|
|
243
|
+
base = ctx.workspace_dir.resolve()
|
|
244
|
+
target = (base / path).resolve()
|
|
245
|
+
# Path-traversal guard
|
|
246
|
+
try:
|
|
247
|
+
target.relative_to(base)
|
|
248
|
+
except ValueError:
|
|
249
|
+
return {
|
|
250
|
+
"error": "path escapes workspace_dir",
|
|
251
|
+
"path": str(path),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if not target.exists():
|
|
255
|
+
return {"files": [], "path": str(path)}
|
|
256
|
+
|
|
257
|
+
entries: List[Dict[str, Any]] = []
|
|
258
|
+
if target.is_file():
|
|
259
|
+
files_iter = [target]
|
|
260
|
+
else:
|
|
261
|
+
files_iter = sorted(target.rglob(pattern))
|
|
262
|
+
|
|
263
|
+
for p in files_iter:
|
|
264
|
+
if not p.is_file():
|
|
265
|
+
continue
|
|
266
|
+
try:
|
|
267
|
+
rel = str(p.relative_to(base))
|
|
268
|
+
info: Dict[str, Any] = {
|
|
269
|
+
"path": rel,
|
|
270
|
+
"size": p.stat().st_size,
|
|
271
|
+
"mtime": p.stat().st_mtime,
|
|
272
|
+
}
|
|
273
|
+
if read and p.stat().st_size <= max_bytes:
|
|
274
|
+
try:
|
|
275
|
+
info["content"] = p.read_text(
|
|
276
|
+
encoding="utf-8", errors="replace"
|
|
277
|
+
)
|
|
278
|
+
except OSError as exc:
|
|
279
|
+
info["read_error"] = str(exc)
|
|
280
|
+
entries.append(info)
|
|
281
|
+
except OSError:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
if len(entries) >= 1000:
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
return {"files": entries, "path": str(path)}
|
|
288
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
289
|
+
logger.exception("[CC-Agent MCP] getWorkspaceFiles failed")
|
|
290
|
+
return {"error": str(exc), "path": str(path)}
|
|
291
|
+
|
|
292
|
+
@mcp.tool(
|
|
293
|
+
name="listSkills",
|
|
294
|
+
description=(
|
|
295
|
+
"List skills connected to the parent agent node. Returns "
|
|
296
|
+
"metadata only (~100 tokens per skill). Call `getSkill(name)` "
|
|
297
|
+
"to fetch the full instructions."
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
def list_skills() -> Dict[str, Any]:
|
|
301
|
+
ctx = _require_batch()
|
|
302
|
+
try:
|
|
303
|
+
from services.skill_loader import get_skill_loader
|
|
304
|
+
loader = get_skill_loader()
|
|
305
|
+
registry = loader.scan_skills()
|
|
306
|
+
results = []
|
|
307
|
+
for name in sorted(ctx.connected_skill_names):
|
|
308
|
+
meta = registry.get(name)
|
|
309
|
+
if meta is None:
|
|
310
|
+
continue
|
|
311
|
+
results.append({
|
|
312
|
+
"name": meta.name,
|
|
313
|
+
"description": meta.description,
|
|
314
|
+
"allowed_tools": list(meta.allowed_tools),
|
|
315
|
+
"category": (
|
|
316
|
+
meta.metadata.get("category")
|
|
317
|
+
if isinstance(meta.metadata, dict) else None
|
|
318
|
+
),
|
|
319
|
+
})
|
|
320
|
+
return {"skills": results}
|
|
321
|
+
except Exception as exc: # pragma: no cover
|
|
322
|
+
logger.exception("[CC-Agent MCP] listSkills failed")
|
|
323
|
+
return {"error": str(exc), "skills": []}
|
|
324
|
+
|
|
325
|
+
@mcp.tool(
|
|
326
|
+
name="getSkill",
|
|
327
|
+
description=(
|
|
328
|
+
"Fetch full content for one skill: instructions (markdown), "
|
|
329
|
+
"scripts (executable code samples), and references (extra "
|
|
330
|
+
"docs). The skill must be connected to the parent agent node."
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
def get_skill(name: str) -> Dict[str, Any]:
|
|
334
|
+
ctx = _require_batch()
|
|
335
|
+
if name not in ctx.connected_skill_names:
|
|
336
|
+
return {
|
|
337
|
+
"error": f"skill {name!r} is not connected to this agent node",
|
|
338
|
+
"name": name,
|
|
339
|
+
}
|
|
340
|
+
try:
|
|
341
|
+
from services.skill_loader import get_skill_loader
|
|
342
|
+
loader = get_skill_loader()
|
|
343
|
+
skill = loader.load_skill(name)
|
|
344
|
+
if skill is None:
|
|
345
|
+
return {"error": f"skill {name!r} not found", "name": name}
|
|
346
|
+
return {
|
|
347
|
+
"name": skill.metadata.name,
|
|
348
|
+
"description": skill.metadata.description,
|
|
349
|
+
"instructions": skill.instructions,
|
|
350
|
+
"allowed_tools": list(skill.metadata.allowed_tools),
|
|
351
|
+
"metadata": dict(skill.metadata.metadata) if skill.metadata.metadata else {},
|
|
352
|
+
"scripts": dict(skill.scripts),
|
|
353
|
+
"references": dict(skill.references),
|
|
354
|
+
# `assets` (binary) excluded by default — too big for MCP responses.
|
|
355
|
+
}
|
|
356
|
+
except Exception as exc: # pragma: no cover
|
|
357
|
+
logger.exception("[CC-Agent MCP] getSkill failed for %r", name)
|
|
358
|
+
return {"error": str(exc), "name": name}
|
|
359
|
+
|
|
360
|
+
@mcp.tool(
|
|
361
|
+
name="getCredential",
|
|
362
|
+
description=(
|
|
363
|
+
"Fetch a credential by provider name. Only credentials in the "
|
|
364
|
+
"batch's allowlist are returned; everything else returns 403. "
|
|
365
|
+
"Use sparingly — prefer the CLI's own auth where possible."
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
async def get_credential(name: str) -> Dict[str, Any]:
|
|
369
|
+
ctx = _require_batch()
|
|
370
|
+
if name not in ctx.allowed_credentials:
|
|
371
|
+
return {
|
|
372
|
+
"error": f"credential {name!r} not in allowlist for this batch",
|
|
373
|
+
"name": name,
|
|
374
|
+
"status": 403,
|
|
375
|
+
}
|
|
376
|
+
try:
|
|
377
|
+
from core.container import container
|
|
378
|
+
auth = container.auth_service()
|
|
379
|
+
value = await auth.get_api_key(name)
|
|
380
|
+
if not value:
|
|
381
|
+
return {
|
|
382
|
+
"error": f"credential {name!r} not configured",
|
|
383
|
+
"name": name,
|
|
384
|
+
"status": 404,
|
|
385
|
+
}
|
|
386
|
+
return {"name": name, "value": value}
|
|
387
|
+
except Exception as exc: # pragma: no cover
|
|
388
|
+
logger.exception("[CC-Agent MCP] getCredential failed for %r", name)
|
|
389
|
+
return {"error": str(exc), "name": name, "status": 500}
|
|
390
|
+
|
|
391
|
+
# Per-batch workflow tools are exposed dynamically on
|
|
392
|
+
# `register_batch` via `_expose_workflow_tools` — each connected
|
|
393
|
+
# node lands as its own `mcp__machinaos__<node_type>` entry. No
|
|
394
|
+
# generic `listMachinaOsTools` / `callMachinaOsTool` wrapper.
|
|
395
|
+
|
|
396
|
+
@mcp.tool(
|
|
397
|
+
name="broadcastLog",
|
|
398
|
+
description=(
|
|
399
|
+
"Write a log line to the MachinaOs Terminal tab. Use to "
|
|
400
|
+
"surface intermediate progress that would otherwise be lost "
|
|
401
|
+
"between CLI sessions."
|
|
402
|
+
),
|
|
403
|
+
)
|
|
404
|
+
async def broadcast_log(
|
|
405
|
+
message: str,
|
|
406
|
+
level: str = "info",
|
|
407
|
+
source: Optional[str] = None,
|
|
408
|
+
) -> Dict[str, Any]:
|
|
409
|
+
ctx = _require_batch()
|
|
410
|
+
if level not in ("debug", "info", "warning", "error"):
|
|
411
|
+
level = "info"
|
|
412
|
+
try:
|
|
413
|
+
broadcaster = ctx.broadcaster
|
|
414
|
+
if broadcaster is None:
|
|
415
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
416
|
+
broadcaster = get_status_broadcaster()
|
|
417
|
+
payload = {
|
|
418
|
+
"source": source or f"mcp:{ctx.node_id}",
|
|
419
|
+
"level": level,
|
|
420
|
+
"message": message[:5000],
|
|
421
|
+
}
|
|
422
|
+
await broadcaster.broadcast_terminal_log(payload)
|
|
423
|
+
return {"success": True}
|
|
424
|
+
except Exception as exc: # pragma: no cover
|
|
425
|
+
logger.exception("[CC-Agent MCP] broadcastLog failed")
|
|
426
|
+
return {"error": str(exc)}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ---------------------------------------------------------------------------
|
|
430
|
+
# ASGI sub-app factory (mounted in main.py lifespan)
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
_app_singleton: Optional[Any] = None # Starlette app
|
|
434
|
+
_mcp_singleton: Optional[Any] = None # FastMCP instance — used by per-batch
|
|
435
|
+
# dynamic tool (un)registration.
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def get_mcp_app() -> Any:
|
|
439
|
+
"""Return the Starlette/ASGI app to mount under `/mcp/ide`.
|
|
440
|
+
|
|
441
|
+
Idempotent — multiple calls return the same instance, so the FastAPI
|
|
442
|
+
lifespan can wire it without worrying about duplicate registration.
|
|
443
|
+
"""
|
|
444
|
+
global _app_singleton, _mcp_singleton
|
|
445
|
+
if _app_singleton is not None:
|
|
446
|
+
return _app_singleton
|
|
447
|
+
|
|
448
|
+
from mcp.server.fastmcp import FastMCP
|
|
449
|
+
|
|
450
|
+
mcp = FastMCP(
|
|
451
|
+
name="machinaos-cli-agent",
|
|
452
|
+
instructions=(
|
|
453
|
+
"MachinaOs IDE MCP server. Exposes workspace files, connected "
|
|
454
|
+
"skills, scoped credentials, and a Terminal-tab log channel "
|
|
455
|
+
"to the calling CLI session."
|
|
456
|
+
),
|
|
457
|
+
# FastMCP's HTTP path defaults are fine; we mount the whole app
|
|
458
|
+
# under /mcp/ide externally.
|
|
459
|
+
)
|
|
460
|
+
_build_tools(mcp)
|
|
461
|
+
_mcp_singleton = mcp
|
|
462
|
+
|
|
463
|
+
asgi_app = mcp.streamable_http_app()
|
|
464
|
+
asgi_app.add_middleware(_BearerAuthMiddleware)
|
|
465
|
+
|
|
466
|
+
_app_singleton = asgi_app
|
|
467
|
+
return _app_singleton
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
# Test/diagnostic helpers (used by tests + verification step #11/#12)
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
def _reset_for_tests() -> None: # pragma: no cover
|
|
475
|
+
"""Wipe the token registry + per-batch workflow-tool refcounts.
|
|
476
|
+
ONLY use in tests."""
|
|
477
|
+
global _app_singleton, _mcp_singleton
|
|
478
|
+
_active_tokens.clear()
|
|
479
|
+
_app_singleton = None
|
|
480
|
+
_mcp_singleton = None
|
|
481
|
+
from services.cli_agent.workflow_tools import _reset_for_tests as _wt_reset
|
|
482
|
+
_wt_reset()
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Protocol for AI CLI providers (Claude Code, Codex, Gemini).
|
|
2
|
+
|
|
3
|
+
Mirrors `services/llm/protocol.py` shape: a structurally-typed Protocol
|
|
4
|
+
with a small, explicit surface. Each concrete provider lives under
|
|
5
|
+
`providers/<vendor>.py`.
|
|
6
|
+
|
|
7
|
+
The framework spawns N parallel CLI sessions (one `AICliSession` each,
|
|
8
|
+
backed by `BaseProcessSupervisor`) over a list of tasks. Per-CLI
|
|
9
|
+
differences (argv, JSON event schema, auth handling, feature support)
|
|
10
|
+
are isolated to the provider; the session/pool/service layer is generic.
|
|
11
|
+
|
|
12
|
+
Auth: native CLI handles its own tokens. We only trigger the login flow
|
|
13
|
+
(`login_argv()`) and detect logged-in state (`auth_status_argv()` +
|
|
14
|
+
`detect_auth_error()`). No credential wrapping, no `~/.claude-machina/`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Shared data types
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CanonicalUsage:
|
|
30
|
+
"""Vendor-normalised token counts.
|
|
31
|
+
|
|
32
|
+
Pattern from Hermes `agent/usage_pricing.py:CanonicalUsage` — every
|
|
33
|
+
provider's usage shape (Anthropic vs Codex vs OpenAI handle cache
|
|
34
|
+
differently) maps into this so the existing `services/pricing.py`
|
|
35
|
+
can compute USD without per-vendor branches.
|
|
36
|
+
"""
|
|
37
|
+
input_tokens: int = 0
|
|
38
|
+
output_tokens: int = 0
|
|
39
|
+
cache_read: int = 0
|
|
40
|
+
cache_write: int = 0
|
|
41
|
+
reasoning_tokens: int = 0
|
|
42
|
+
request_count: int = 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SessionResult:
|
|
47
|
+
"""Per-task result returned by `AICliSession`.
|
|
48
|
+
|
|
49
|
+
Shared keys are the schema; vendor extras live in `provider_data`.
|
|
50
|
+
"""
|
|
51
|
+
task_id: str
|
|
52
|
+
session_id: Optional[str] = None
|
|
53
|
+
provider: str = ""
|
|
54
|
+
prompt: str = ""
|
|
55
|
+
branch: Optional[str] = None
|
|
56
|
+
worktree_path: Optional[str] = None
|
|
57
|
+
response: str = ""
|
|
58
|
+
cost_usd: Optional[float] = None
|
|
59
|
+
duration_ms: Optional[int] = None
|
|
60
|
+
num_turns: Optional[int] = None
|
|
61
|
+
tool_calls: int = 0
|
|
62
|
+
canonical_usage: CanonicalUsage = field(default_factory=CanonicalUsage)
|
|
63
|
+
provider_data: Dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
success: bool = False
|
|
65
|
+
error: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class BatchResult:
|
|
70
|
+
"""Aggregated result returned by `AICliService.run_batch()`."""
|
|
71
|
+
tasks: List[SessionResult] = field(default_factory=list)
|
|
72
|
+
n_tasks: int = 0
|
|
73
|
+
n_succeeded: int = 0
|
|
74
|
+
n_failed: int = 0
|
|
75
|
+
total_cost_usd: Optional[float] = None
|
|
76
|
+
wall_clock_ms: int = 0
|
|
77
|
+
budget_remaining_usd: Optional[float] = None
|
|
78
|
+
provider: str = ""
|
|
79
|
+
timestamp: str = ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Provider protocol (structural typing)
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@runtime_checkable
|
|
87
|
+
class AICliProvider(Protocol):
|
|
88
|
+
"""Structural Protocol for an AI CLI provider.
|
|
89
|
+
|
|
90
|
+
Concrete classes:
|
|
91
|
+
- `providers.anthropic_claude.AnthropicClaudeProvider`
|
|
92
|
+
- `providers.openai_codex.OpenAICodexProvider`
|
|
93
|
+
- `providers.google_gemini.GoogleGeminiProvider` (v2 stub)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
name: str # "claude" | "codex" | "gemini"
|
|
97
|
+
package_name: str # npm package
|
|
98
|
+
binary_name: str # "claude" | "codex" | "gemini"
|
|
99
|
+
ide_lock_env_var: Optional[str] # CLAUDE_IDE_LOCK | GEMINI_IDE_LOCK | None
|
|
100
|
+
ide_lockfile_dir: Optional[Path] # ~/.claude/ide | <tmpdir>/gemini/ide
|
|
101
|
+
|
|
102
|
+
# ---- spawn surface ---------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def binary_path(self) -> Path: ...
|
|
105
|
+
# Resolve the CLI binary. Resolution chain (Composio pattern):
|
|
106
|
+
# 1) shutil.which(<binary_name>)
|
|
107
|
+
# 2) `npx --yes <package_name>` shim path
|
|
108
|
+
# Raises FileNotFoundError if neither is available.
|
|
109
|
+
|
|
110
|
+
def headless_argv(self, task: Any, *, defaults: Dict[str, Any]) -> List[str]: ...
|
|
111
|
+
# Build the full argv (binary + flags) for a headless run of
|
|
112
|
+
# `task` (a `<Provider>TaskSpec` Pydantic model). `defaults`
|
|
113
|
+
# comes from `ai_cli_providers.json` for this provider.
|
|
114
|
+
|
|
115
|
+
# ---- native auth (no token wrapping) --------------------------------
|
|
116
|
+
|
|
117
|
+
def login_argv(self) -> List[str]: ...
|
|
118
|
+
# CLI's own login command, e.g. ["claude", "login"]. Spawned
|
|
119
|
+
# interactively from the Credentials Modal. CLI stores its own
|
|
120
|
+
# credentials in `~/.claude/`, `~/.codex/`, `~/.gemini/`.
|
|
121
|
+
|
|
122
|
+
def auth_status_argv(self) -> Optional[List[str]]: ...
|
|
123
|
+
# No-op invocation to verify auth, e.g. ["claude", "--print", "-p", "ok"].
|
|
124
|
+
# Returns None if no cheap probe exists; in that case the framework
|
|
125
|
+
# infers from the first session's stderr.
|
|
126
|
+
|
|
127
|
+
def detect_auth_error(self, stderr: str, exit_code: int) -> bool: ...
|
|
128
|
+
# Match "not logged in" patterns:
|
|
129
|
+
# Claude: "Please run 'claude login'"
|
|
130
|
+
# Codex: HTTP 401 / "OPENAI_API_KEY not set"
|
|
131
|
+
# Gemini: exit_code == 1 (FatalAuthenticationError)
|
|
132
|
+
|
|
133
|
+
# ---- streaming output parsing ---------------------------------------
|
|
134
|
+
|
|
135
|
+
def parse_event(self, line: str) -> Optional[Dict[str, Any]]: ...
|
|
136
|
+
# Parse a single NDJSON line from stdout. Return None for
|
|
137
|
+
# un-parseable garbage.
|
|
138
|
+
|
|
139
|
+
def is_final_event(self, event: Dict[str, Any]) -> bool: ...
|
|
140
|
+
# True if this event marks end-of-task. For Claude: `type=="result"`.
|
|
141
|
+
# For Gemini: `type=="result"`. For Codex: `type=="complete"` or
|
|
142
|
+
# heuristic fallback.
|
|
143
|
+
|
|
144
|
+
def event_to_session_result(
|
|
145
|
+
self,
|
|
146
|
+
events: List[Dict[str, Any]],
|
|
147
|
+
stderr: str,
|
|
148
|
+
exit_code: int,
|
|
149
|
+
) -> Dict[str, Any]: ...
|
|
150
|
+
# Reconstruct a partial dict of `SessionResult` fields from the
|
|
151
|
+
# event stream. Returns:
|
|
152
|
+
# {
|
|
153
|
+
# "session_id": ..., "response": ..., "cost_usd": ...,
|
|
154
|
+
# "duration_ms": ..., "num_turns": ..., "tool_calls": ...,
|
|
155
|
+
# "success": bool, "error": Optional[str],
|
|
156
|
+
# "canonical_usage": CanonicalUsage,
|
|
157
|
+
# "provider_data": {<vendor-specific>},
|
|
158
|
+
# }
|
|
159
|
+
# provider_data carries vendor-only metadata (Anthropic
|
|
160
|
+
# reasoning_details, Codex call_id, Gemini extra_content) without
|
|
161
|
+
# bloating the shared schema. Pattern from
|
|
162
|
+
# Hermes agent/transports/types.py NormalizedResponse.
|
|
163
|
+
|
|
164
|
+
def canonical_usage(self, events: List[Dict[str, Any]]) -> CanonicalUsage: ...
|
|
165
|
+
# Normalise vendor token-counting into the shared `CanonicalUsage`
|
|
166
|
+
# shape. Pattern from Hermes agent/usage_pricing.py.
|
|
167
|
+
|
|
168
|
+
# ---- feature gating --------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def supports(self, feature: str) -> bool: ...
|
|
171
|
+
# Feature flags consulted by the session/service layer.
|
|
172
|
+
# Recognised: "max_budget", "max_turns", "session_id", "resume",
|
|
173
|
+
# "mcp_runtime", "json_cost", "ide_lockfile", "sandbox".
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Concrete `AICliProvider` implementations.
|
|
2
|
+
|
|
3
|
+
- `anthropic_claude.AnthropicClaudeProvider` — full v1 surface
|
|
4
|
+
- `openai_codex.OpenAICodexProvider` — sandbox-first, no session
|
|
5
|
+
- `google_gemini.GoogleGeminiProvider` — v2 stub raising NotImplementedError
|
|
6
|
+
|
|
7
|
+
Imports are intentionally not eager — `factory.create_cli_provider()`
|
|
8
|
+
lazy-imports each one.
|
|
9
|
+
"""
|