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,227 @@
|
|
|
1
|
+
"""Pydantic task specs (discriminated union) + result models.
|
|
2
|
+
|
|
3
|
+
The discriminated union lets each plugin's `Params.tasks` be hard-typed
|
|
4
|
+
to one variant (e.g. `list[ClaudeTaskSpec]`), so the LLM tool schema
|
|
5
|
+
fast-path at `services/ai.py:2898` produces a clean per-provider schema
|
|
6
|
+
with no `$defs`/`$ref`.
|
|
7
|
+
|
|
8
|
+
v1 ships Claude + Codex plugins fully wired. `GeminiTaskSpec` is part of
|
|
9
|
+
the union (and exported) so the type system, factory dispatch keys, and
|
|
10
|
+
config JSON are ready, but `create_cli_provider("gemini")` raises
|
|
11
|
+
`NotImplementedError`. v2 swaps the stub for the real impl.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
|
|
20
|
+
from services.cli_agent.protocol import CanonicalUsage
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Task specs (discriminated union)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class BaseAICliTaskSpec(BaseModel):
|
|
28
|
+
"""Shared fields for every CLI task."""
|
|
29
|
+
task_id: Optional[str] = Field(
|
|
30
|
+
default=None,
|
|
31
|
+
description="Auto-assigned `t_<8hex>` if omitted.",
|
|
32
|
+
)
|
|
33
|
+
prompt: str = Field(
|
|
34
|
+
...,
|
|
35
|
+
description="The task prompt sent to the CLI.",
|
|
36
|
+
json_schema_extra={"rows": 4},
|
|
37
|
+
)
|
|
38
|
+
branch: Optional[str] = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description="Branch name for the per-task git worktree. "
|
|
41
|
+
"Auto-named `machina/<task_id>` if omitted.",
|
|
42
|
+
)
|
|
43
|
+
model: Optional[str] = Field(default=None)
|
|
44
|
+
timeout_seconds: int = Field(
|
|
45
|
+
default=600, ge=10, le=3600,
|
|
46
|
+
description="Hard timeout per task. On expiry the session is "
|
|
47
|
+
"terminate_then_kill'd and a diagnostic dump is "
|
|
48
|
+
"written to ~/.claude-machina/logs/.",
|
|
49
|
+
)
|
|
50
|
+
system_prompt: Optional[str] = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
json_schema_extra={"rows": 3},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# `extra="forbid"` surfaces typo'd task fields as Pydantic
|
|
56
|
+
# ValidationError at the spec boundary instead of silently dropping
|
|
57
|
+
# them and confusing downstream consumers.
|
|
58
|
+
model_config = ConfigDict(extra="forbid")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ClaudeTaskSpec(BaseAICliTaskSpec):
|
|
62
|
+
"""Claude Code CLI task. Full feature set: sessions, resume, budget,
|
|
63
|
+
turns, allowed_tools, permission_mode."""
|
|
64
|
+
provider: Literal["claude"] = "claude"
|
|
65
|
+
session_id: Optional[str] = Field(
|
|
66
|
+
default=None,
|
|
67
|
+
description="Start a named session. Pair with `resume_session_id` "
|
|
68
|
+
"to chain conversations.",
|
|
69
|
+
)
|
|
70
|
+
resume_session_id: Optional[str] = Field(
|
|
71
|
+
default=None,
|
|
72
|
+
description="Resume from a prior session_id (returned in a previous "
|
|
73
|
+
"task's `result` event).",
|
|
74
|
+
)
|
|
75
|
+
max_turns: Optional[int] = Field(
|
|
76
|
+
default=None, ge=1,
|
|
77
|
+
description="Per-task turn cap. Defaults to provider config.",
|
|
78
|
+
)
|
|
79
|
+
max_budget_usd: Optional[float] = Field(
|
|
80
|
+
default=None, ge=0,
|
|
81
|
+
description="Per-task USD budget. Defaults to provider config.",
|
|
82
|
+
)
|
|
83
|
+
allowed_tools: Optional[str] = Field(
|
|
84
|
+
default=None,
|
|
85
|
+
description="Comma-separated tool list. Defaults to "
|
|
86
|
+
"Read,Edit,Bash,Glob,Grep,Write.",
|
|
87
|
+
)
|
|
88
|
+
permission_mode: Literal["default", "acceptEdits", "plan", "auto", "dontAsk", "bypassPermissions"] = "acceptEdits"
|
|
89
|
+
|
|
90
|
+
# ---- optional documented CLI flags (cli-reference) ----
|
|
91
|
+
effort: Optional[Literal["low", "medium", "high", "xhigh", "max"]] = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="Reasoning-effort level. Available levels depend on the model.",
|
|
94
|
+
)
|
|
95
|
+
fallback_model: Optional[str] = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description="Fallback model when the primary is overloaded (print mode).",
|
|
98
|
+
)
|
|
99
|
+
add_dir: List[str] = Field(
|
|
100
|
+
default_factory=list,
|
|
101
|
+
description="Extra working directories the CLI may read/edit.",
|
|
102
|
+
)
|
|
103
|
+
disallowed_tools: Optional[str] = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
description="Comma-separated tools removed from Claude's context.",
|
|
106
|
+
)
|
|
107
|
+
agent: Optional[str] = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description="Override the configured `agent` setting for this task.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CodexTaskSpec(BaseAICliTaskSpec):
|
|
114
|
+
"""OpenAI Codex CLI task. Sandbox-first; no session/resume/budget/turns."""
|
|
115
|
+
provider: Literal["codex"] = "codex"
|
|
116
|
+
sandbox: Literal["read-only", "workspace-write", "danger-full-access"] = (
|
|
117
|
+
"workspace-write"
|
|
118
|
+
)
|
|
119
|
+
ask_for_approval: Literal["untrusted", "on-request", "never"] = "never"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GeminiTaskSpec(BaseAICliTaskSpec):
|
|
123
|
+
"""Google Gemini CLI task (v2 — stub provider in v1).
|
|
124
|
+
|
|
125
|
+
Schema lives in v1 so the discriminated union JSON Schema for the
|
|
126
|
+
LLM tool fast-path doesn't change when v2 lands.
|
|
127
|
+
"""
|
|
128
|
+
provider: Literal["gemini"] = "gemini"
|
|
129
|
+
session_id: Optional[str] = None
|
|
130
|
+
resume: Optional[str] = Field(
|
|
131
|
+
default=None,
|
|
132
|
+
description='"latest" | "<index>" | "<UUID>"',
|
|
133
|
+
)
|
|
134
|
+
yolo: bool = False
|
|
135
|
+
sandbox: bool = False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
AICliTaskSpec = Annotated[
|
|
139
|
+
Union[ClaudeTaskSpec, CodexTaskSpec, GeminiTaskSpec],
|
|
140
|
+
Field(discriminator="provider"),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Result models (Pydantic for serialisation; mirror dataclasses in protocol.py)
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
class CanonicalUsagePydantic(BaseModel):
|
|
149
|
+
"""Pydantic mirror of `protocol.CanonicalUsage` for output serialisation."""
|
|
150
|
+
input_tokens: int = 0
|
|
151
|
+
output_tokens: int = 0
|
|
152
|
+
cache_read: int = 0
|
|
153
|
+
cache_write: int = 0
|
|
154
|
+
reasoning_tokens: int = 0
|
|
155
|
+
request_count: int = 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SessionResultModel(BaseModel):
|
|
159
|
+
"""Per-task result, JSON-serialisable."""
|
|
160
|
+
task_id: str
|
|
161
|
+
session_id: Optional[str] = None
|
|
162
|
+
provider: str = ""
|
|
163
|
+
prompt: str = ""
|
|
164
|
+
branch: Optional[str] = None
|
|
165
|
+
worktree_path: Optional[str] = None
|
|
166
|
+
response: str = ""
|
|
167
|
+
cost_usd: Optional[float] = None
|
|
168
|
+
duration_ms: Optional[int] = None
|
|
169
|
+
num_turns: Optional[int] = None
|
|
170
|
+
tool_calls: int = 0
|
|
171
|
+
canonical_usage: CanonicalUsagePydantic = Field(
|
|
172
|
+
default_factory=CanonicalUsagePydantic,
|
|
173
|
+
)
|
|
174
|
+
provider_data: Dict[str, Any] = Field(default_factory=dict)
|
|
175
|
+
success: bool = False
|
|
176
|
+
error: Optional[str] = None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class BatchSummary(BaseModel):
|
|
180
|
+
"""Aggregated batch summary."""
|
|
181
|
+
n_tasks: int = 0
|
|
182
|
+
n_succeeded: int = 0
|
|
183
|
+
n_failed: int = 0
|
|
184
|
+
total_cost_usd: Optional[float] = None
|
|
185
|
+
wall_clock_ms: int = 0
|
|
186
|
+
budget_remaining_usd: Optional[float] = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class BatchResultModel(BaseModel):
|
|
190
|
+
"""Top-level batch result returned by `run_batch()`."""
|
|
191
|
+
tasks: List[SessionResultModel] = Field(default_factory=list)
|
|
192
|
+
summary: BatchSummary = Field(default_factory=BatchSummary)
|
|
193
|
+
provider: str = ""
|
|
194
|
+
timestamp: str = ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Helpers
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def session_result_to_model(sr: Any) -> SessionResultModel:
|
|
202
|
+
"""Convert a `protocol.SessionResult` dataclass to its Pydantic mirror."""
|
|
203
|
+
cu = CanonicalUsagePydantic(
|
|
204
|
+
input_tokens=sr.canonical_usage.input_tokens,
|
|
205
|
+
output_tokens=sr.canonical_usage.output_tokens,
|
|
206
|
+
cache_read=sr.canonical_usage.cache_read,
|
|
207
|
+
cache_write=sr.canonical_usage.cache_write,
|
|
208
|
+
reasoning_tokens=sr.canonical_usage.reasoning_tokens,
|
|
209
|
+
request_count=sr.canonical_usage.request_count,
|
|
210
|
+
)
|
|
211
|
+
return SessionResultModel(
|
|
212
|
+
task_id=sr.task_id,
|
|
213
|
+
session_id=sr.session_id,
|
|
214
|
+
provider=sr.provider,
|
|
215
|
+
prompt=sr.prompt,
|
|
216
|
+
branch=sr.branch,
|
|
217
|
+
worktree_path=sr.worktree_path,
|
|
218
|
+
response=sr.response,
|
|
219
|
+
cost_usd=sr.cost_usd,
|
|
220
|
+
duration_ms=sr.duration_ms,
|
|
221
|
+
num_turns=sr.num_turns,
|
|
222
|
+
tool_calls=sr.tool_calls,
|
|
223
|
+
canonical_usage=cu,
|
|
224
|
+
provider_data=sr.provider_data,
|
|
225
|
+
success=sr.success,
|
|
226
|
+
error=sr.error,
|
|
227
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Per-batch workflow-tool bridge for the FastMCP server.
|
|
2
|
+
|
|
3
|
+
When an agent batch wires nodes through ``input-tools``, each connected
|
|
4
|
+
node is exposed on the FastMCP server as its own
|
|
5
|
+
``mcp__machinaos__<node_type>`` entry. The spawned ``claude -p`` sees
|
|
6
|
+
those tools on the very first ``tools/list`` and can invoke them
|
|
7
|
+
directly — no two-step generic-wrapper indirection.
|
|
8
|
+
|
|
9
|
+
Schema generation is delegated to FastMCP: we build a function whose
|
|
10
|
+
``inspect.signature`` mirrors the plugin's Pydantic ``Params`` field-
|
|
11
|
+
for-field, so FastMCP advertises a flat ``inputSchema`` matching the
|
|
12
|
+
plugin Params. Per-batch scoping is enforced inside the handler via
|
|
13
|
+
``_require_batch()`` so concurrent batches sharing the same tool name
|
|
14
|
+
are isolated; refcounts (``add_tool`` on first wire, ``remove_tool`` on
|
|
15
|
+
last unwire) keep the FastMCP registry tidy.
|
|
16
|
+
|
|
17
|
+
Public API:
|
|
18
|
+
- :func:`expose_workflow_tools(connected_tools)` — call from
|
|
19
|
+
``mcp_server.register_batch``
|
|
20
|
+
- :func:`unexpose_workflow_tools(connected_tools)` — call from
|
|
21
|
+
``mcp_server.unregister_batch``
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import inspect
|
|
28
|
+
from typing import Any, Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from core.logging import get_logger
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# node_type -> count of active batches that wired it. Tools are added
|
|
36
|
+
# to FastMCP on first wire, removed on last unwire — per-handler scope
|
|
37
|
+
# checks ``_require_batch`` against the calling batch's connected_tools.
|
|
38
|
+
_active_tool_refcounts: Dict[str, int] = {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def expose_workflow_tools(connected_tools: List[Dict[str, Any]]) -> None:
|
|
42
|
+
"""Add one MCP tool per connected workflow-tool node."""
|
|
43
|
+
mcp = _get_mcp()
|
|
44
|
+
if mcp is None or not connected_tools:
|
|
45
|
+
return
|
|
46
|
+
from services.node_registry import get_node_class
|
|
47
|
+
|
|
48
|
+
for entry in connected_tools:
|
|
49
|
+
node_type = entry.get("node_type")
|
|
50
|
+
if not node_type:
|
|
51
|
+
continue
|
|
52
|
+
prev = _active_tool_refcounts.get(node_type, 0)
|
|
53
|
+
_active_tool_refcounts[node_type] = prev + 1
|
|
54
|
+
if prev > 0:
|
|
55
|
+
continue # already exposed by another concurrent batch
|
|
56
|
+
cls = get_node_class(node_type)
|
|
57
|
+
if cls is None or getattr(cls, "Params", None) is None:
|
|
58
|
+
logger.warning(
|
|
59
|
+
"[CC-Agent MCP] cannot expose %s: class or Params missing",
|
|
60
|
+
node_type,
|
|
61
|
+
)
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
handler = _build_handler(node_type, cls.Params)
|
|
65
|
+
mcp.add_tool(
|
|
66
|
+
handler, name=node_type,
|
|
67
|
+
description=(getattr(cls, "description", None)
|
|
68
|
+
or f"MachinaOs workflow tool: {node_type}"),
|
|
69
|
+
)
|
|
70
|
+
logger.info("[CC-Agent MCP] exposed mcp__machinaos__%s", node_type)
|
|
71
|
+
except Exception as exc: # pragma: no cover
|
|
72
|
+
logger.warning("[CC-Agent MCP] add_tool(%s) failed: %s", node_type, exc)
|
|
73
|
+
_schedule_list_changed_notify()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def unexpose_workflow_tools(connected_tools: List[Dict[str, Any]]) -> None:
|
|
77
|
+
"""Decrement refcount; remove the MCP tool when no batch references it."""
|
|
78
|
+
mcp = _get_mcp()
|
|
79
|
+
if mcp is None:
|
|
80
|
+
return
|
|
81
|
+
for entry in connected_tools:
|
|
82
|
+
node_type = entry.get("node_type")
|
|
83
|
+
if not node_type:
|
|
84
|
+
continue
|
|
85
|
+
remaining = _active_tool_refcounts.get(node_type, 0) - 1
|
|
86
|
+
if remaining > 0:
|
|
87
|
+
_active_tool_refcounts[node_type] = remaining
|
|
88
|
+
continue
|
|
89
|
+
_active_tool_refcounts.pop(node_type, None)
|
|
90
|
+
try:
|
|
91
|
+
mcp.remove_tool(node_type)
|
|
92
|
+
logger.info("[CC-Agent MCP] removed mcp__machinaos__%s", node_type)
|
|
93
|
+
except Exception as exc: # pragma: no cover
|
|
94
|
+
logger.debug("[CC-Agent MCP] remove_tool(%s): %s", node_type, exc)
|
|
95
|
+
_schedule_list_changed_notify()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Internals
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _get_mcp() -> Optional[Any]:
|
|
103
|
+
"""Late import to avoid a circular import with ``mcp_server``."""
|
|
104
|
+
from services.cli_agent import mcp_server
|
|
105
|
+
return mcp_server._mcp_singleton
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _schedule_list_changed_notify() -> None:
|
|
109
|
+
"""Fire-and-forget ``notifications/tools/list_changed`` to the
|
|
110
|
+
connected MCP client.
|
|
111
|
+
|
|
112
|
+
FastMCP does NOT emit this automatically on ``add_tool`` /
|
|
113
|
+
``remove_tool`` (verified at
|
|
114
|
+
``mcp/server/fastmcp/tools/tool_manager.py`` — both methods only
|
|
115
|
+
mutate the ``_tools`` dict, with no notification dispatch). Without
|
|
116
|
+
this manual notify, tools registered after the agent's first
|
|
117
|
+
``tools/list`` request stay invisible until reconnect.
|
|
118
|
+
|
|
119
|
+
Best-effort: requires a running asyncio loop (always true in the
|
|
120
|
+
``service.run_batch`` call path). Failures log at WARN and don't
|
|
121
|
+
abort the batch.
|
|
122
|
+
"""
|
|
123
|
+
mcp = _get_mcp()
|
|
124
|
+
if mcp is None:
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
loop = asyncio.get_running_loop()
|
|
128
|
+
except RuntimeError:
|
|
129
|
+
return # no running loop — sync test path
|
|
130
|
+
|
|
131
|
+
async def _do_notify() -> None:
|
|
132
|
+
try:
|
|
133
|
+
session = (
|
|
134
|
+
getattr(mcp, "session", None)
|
|
135
|
+
or getattr(getattr(mcp, "_mcp_server", None), "session", None)
|
|
136
|
+
)
|
|
137
|
+
if session is not None and hasattr(session, "send_tool_list_changed"):
|
|
138
|
+
await session.send_tool_list_changed()
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
logger.warning(
|
|
141
|
+
"[CC-Agent workflow_tools] tools/list_changed notify failed: %s",
|
|
142
|
+
exc,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
loop.create_task(_do_notify())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_handler(node_type: str, params_cls: type):
|
|
149
|
+
"""Build an async handler whose ``inspect.signature`` mirrors the
|
|
150
|
+
plugin Params field-for-field. FastMCP iterates that signature to
|
|
151
|
+
derive the ``inputSchema``; flat fields → flat MCP arguments.
|
|
152
|
+
"""
|
|
153
|
+
from pydantic_core import PydanticUndefined
|
|
154
|
+
|
|
155
|
+
fields = getattr(params_cls, "model_fields", {}) or {}
|
|
156
|
+
parameters: List[inspect.Parameter] = []
|
|
157
|
+
annotations: Dict[str, Any] = {"return": Dict[str, Any]}
|
|
158
|
+
for fname, finfo in fields.items():
|
|
159
|
+
annotations[fname] = finfo.annotation
|
|
160
|
+
if finfo.is_required():
|
|
161
|
+
default: Any = inspect.Parameter.empty
|
|
162
|
+
elif finfo.default_factory is not None: # type: ignore[truthy-function]
|
|
163
|
+
try:
|
|
164
|
+
default = finfo.default_factory() # materialize once for the signature
|
|
165
|
+
except Exception:
|
|
166
|
+
default = None
|
|
167
|
+
elif finfo.default is PydanticUndefined:
|
|
168
|
+
default = None
|
|
169
|
+
else:
|
|
170
|
+
default = finfo.default
|
|
171
|
+
parameters.append(inspect.Parameter(
|
|
172
|
+
fname, inspect.Parameter.KEYWORD_ONLY,
|
|
173
|
+
annotation=finfo.annotation, default=default,
|
|
174
|
+
))
|
|
175
|
+
|
|
176
|
+
async def _handler(**kwargs: Any) -> Dict[str, Any]:
|
|
177
|
+
from services.cli_agent.mcp_server import _require_batch
|
|
178
|
+
ctx = _require_batch()
|
|
179
|
+
entry = next(
|
|
180
|
+
(t for t in ctx.connected_tools if t.get("node_type") == node_type),
|
|
181
|
+
None,
|
|
182
|
+
)
|
|
183
|
+
if entry is None:
|
|
184
|
+
return {
|
|
185
|
+
"error": f"tool {node_type!r} not connected to this batch",
|
|
186
|
+
"status": 403,
|
|
187
|
+
}
|
|
188
|
+
# Validate via the plugin's own Params model, then dump back to
|
|
189
|
+
# a plain dict for execute_tool. `exclude_unset=True` drops
|
|
190
|
+
# defaults the agent didn't supply.
|
|
191
|
+
try:
|
|
192
|
+
validated = params_cls(**kwargs)
|
|
193
|
+
args = validated.model_dump(exclude_unset=True)
|
|
194
|
+
except Exception:
|
|
195
|
+
args = dict(kwargs)
|
|
196
|
+
logger.info(
|
|
197
|
+
"[CC-Agent MCP %s] node=%s wf=%s args_keys=%s",
|
|
198
|
+
node_type, ctx.node_id, ctx.workflow_id, list(args.keys()),
|
|
199
|
+
)
|
|
200
|
+
from services.handlers.tools import execute_tool
|
|
201
|
+
config: Dict[str, Any] = {
|
|
202
|
+
"node_type": node_type,
|
|
203
|
+
"node_id": entry.get("node_id"),
|
|
204
|
+
"workflow_id": ctx.workflow_id,
|
|
205
|
+
"label": entry.get("label") or node_type,
|
|
206
|
+
"parameters": dict(entry.get("parameters") or {}),
|
|
207
|
+
}
|
|
208
|
+
result = await execute_tool(node_type, args, config)
|
|
209
|
+
if not isinstance(result, dict):
|
|
210
|
+
result = {"result": result}
|
|
211
|
+
if "error" in result:
|
|
212
|
+
logger.warning(
|
|
213
|
+
"[CC-Agent MCP %s] node=%s ERROR: %s",
|
|
214
|
+
node_type, entry.get("node_id"), result.get("error"),
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
logger.info(
|
|
218
|
+
"[CC-Agent MCP %s] node=%s OK (result_keys=%s)",
|
|
219
|
+
node_type, entry.get("node_id"), list(result.keys())[:8],
|
|
220
|
+
)
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
_handler.__name__ = node_type
|
|
224
|
+
_handler.__annotations__ = annotations
|
|
225
|
+
_handler.__signature__ = inspect.Signature( # type: ignore[attr-defined]
|
|
226
|
+
parameters, return_annotation=Dict[str, Any],
|
|
227
|
+
)
|
|
228
|
+
return _handler
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _reset_for_tests() -> None: # pragma: no cover
|
|
232
|
+
"""Wipe the refcount registry. ONLY use in tests."""
|
|
233
|
+
_active_tool_refcounts.clear()
|
|
@@ -44,6 +44,10 @@ class CredentialRegistry:
|
|
|
44
44
|
self._raw: Optional[Dict[str, Any]] = None
|
|
45
45
|
self._resolved_providers: Optional[Dict[str, Dict[str, Any]]] = None
|
|
46
46
|
self._version: Optional[str] = None
|
|
47
|
+
# Bumped on every credential mutation; folded into the version
|
|
48
|
+
# hash so the conditional fetch returns fresh ``stored`` flags.
|
|
49
|
+
# See ``get_version()`` + ``invalidate_version()``.
|
|
50
|
+
self._mutation_seq: int = 0
|
|
47
51
|
|
|
48
52
|
@classmethod
|
|
49
53
|
def get_instance(cls) -> "CredentialRegistry":
|
|
@@ -162,21 +166,42 @@ class CredentialRegistry:
|
|
|
162
166
|
return out
|
|
163
167
|
|
|
164
168
|
def get_version(self) -> str:
|
|
165
|
-
"""Content-sha256 of the resolved catalogue (providers + categories
|
|
169
|
+
"""Content-sha256 of the resolved catalogue (providers + categories +
|
|
170
|
+
mutation counter).
|
|
166
171
|
|
|
167
172
|
Used by clients for warm-start cache invalidation: when the hash
|
|
168
173
|
changes, the client fetches a fresh catalogue; otherwise it serves
|
|
169
174
|
from IndexedDB with zero network traffic.
|
|
175
|
+
|
|
176
|
+
The mutation counter is bumped by ``invalidate_version()`` on every
|
|
177
|
+
credential save / delete / oauth-disconnect — this is what makes
|
|
178
|
+
the conditional fetch ("`since: <prior version>`") actually return
|
|
179
|
+
fresh data after a mutation. Without it the hash would only depend
|
|
180
|
+
on the static catalogue JSON, the version would be constant for
|
|
181
|
+
the life of the process, and the conditional fetch would always
|
|
182
|
+
return ``{unchanged: true}`` even after a key was deleted — leaving
|
|
183
|
+
the per-provider ``stored`` flag stuck at ``true`` on every client.
|
|
170
184
|
"""
|
|
171
185
|
if self._version is None:
|
|
172
186
|
payload = {
|
|
173
187
|
"providers": self.get_all_providers(),
|
|
174
188
|
"categories": self.get_categories(),
|
|
189
|
+
"mutation_seq": self._mutation_seq,
|
|
175
190
|
}
|
|
176
191
|
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
177
192
|
self._version = hashlib.sha256(encoded).hexdigest()
|
|
178
193
|
return self._version
|
|
179
194
|
|
|
195
|
+
def invalidate_version(self) -> None:
|
|
196
|
+
"""Bump the mutation counter so the next ``get_version()`` returns a
|
|
197
|
+
new hash. Call this from every credential mutation path (store /
|
|
198
|
+
remove / oauth-disconnect) so the frontend's conditional fetch
|
|
199
|
+
actually returns fresh ``stored`` flags. Cheap — just a counter
|
|
200
|
+
increment + clearing the cached hash.
|
|
201
|
+
"""
|
|
202
|
+
self._mutation_seq += 1
|
|
203
|
+
self._version = None
|
|
204
|
+
|
|
180
205
|
def get_catalogue(self) -> Dict[str, Any]:
|
|
181
206
|
"""Full payload returned by the `get_credential_catalogue` WebSocket handler."""
|
|
182
207
|
return {
|