@clawpump/claw-agent 0.1.4 → 0.1.6
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/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2294 -3146
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/clawpump_cli.py +3 -3
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +216 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/distribution.py +227 -0
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6188 -975
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +15 -5
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +14 -4
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +53 -5
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +308 -696
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
package/agent/tools/mcp_tool.py
CHANGED
|
@@ -17,8 +17,12 @@ Example config::
|
|
|
17
17
|
command: "npx"
|
|
18
18
|
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
19
19
|
env: {}
|
|
20
|
-
timeout: 120 # per-tool-call timeout in seconds (default:
|
|
20
|
+
timeout: 120 # per-tool-call timeout in seconds (default: 300)
|
|
21
21
|
connect_timeout: 60 # initial connection timeout (default: 60)
|
|
22
|
+
keepalive_interval: 10 # liveness ping cadence in seconds (default:
|
|
23
|
+
# 180). Set below the server's session TTL for
|
|
24
|
+
# servers that GC idle sessions quickly (e.g.
|
|
25
|
+
# Unreal Engine editor MCP, ~15s). Floored at 5s.
|
|
22
26
|
github:
|
|
23
27
|
command: "npx"
|
|
24
28
|
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
@@ -78,6 +82,7 @@ Thread safety:
|
|
|
78
82
|
"""
|
|
79
83
|
|
|
80
84
|
import asyncio
|
|
85
|
+
import contextvars
|
|
81
86
|
import concurrent.futures
|
|
82
87
|
import inspect
|
|
83
88
|
import json
|
|
@@ -89,8 +94,9 @@ import shutil
|
|
|
89
94
|
import sys
|
|
90
95
|
import threading
|
|
91
96
|
import time
|
|
97
|
+
from typing import Callable
|
|
92
98
|
from datetime import datetime
|
|
93
|
-
from typing import Any, Dict, List, Optional
|
|
99
|
+
from typing import Any, Coroutine, Dict, List, Optional
|
|
94
100
|
from urllib.parse import urlparse
|
|
95
101
|
|
|
96
102
|
logger = logging.getLogger(__name__)
|
|
@@ -175,6 +181,7 @@ _MCP_AVAILABLE = False
|
|
|
175
181
|
_MCP_HTTP_AVAILABLE = False
|
|
176
182
|
_MCP_SAMPLING_TYPES = False
|
|
177
183
|
_MCP_NOTIFICATION_TYPES = False
|
|
184
|
+
_MCP_ELICITATION_TYPES = False
|
|
178
185
|
_MCP_MESSAGE_HANDLER_SUPPORTED = False
|
|
179
186
|
# Conservative fallback for SDK builds that don't export LATEST_PROTOCOL_VERSION.
|
|
180
187
|
# Streamable HTTP was introduced by 2025-03-26, so this remains valid for the
|
|
@@ -220,6 +227,16 @@ try:
|
|
|
220
227
|
_MCP_SAMPLING_TYPES = True
|
|
221
228
|
except ImportError:
|
|
222
229
|
logger.debug("MCP sampling types not available -- sampling disabled")
|
|
230
|
+
# Elicitation types -- gated separately for the same reason as sampling.
|
|
231
|
+
# Added in mcp Python SDK 1.11.0 (Jul 2025); servers use elicitation to
|
|
232
|
+
# ask the client for structured input mid-tool-call (e.g. payment
|
|
233
|
+
# authorization). Missing types just disable the feature; everything
|
|
234
|
+
# else keeps working.
|
|
235
|
+
try:
|
|
236
|
+
from mcp.types import ElicitRequestParams, ElicitResult
|
|
237
|
+
_MCP_ELICITATION_TYPES = True
|
|
238
|
+
except ImportError:
|
|
239
|
+
logger.debug("MCP elicitation types not available -- elicitation disabled")
|
|
223
240
|
# Notification types for dynamic tool discovery (tools/list_changed)
|
|
224
241
|
try:
|
|
225
242
|
from mcp.types import (
|
|
@@ -257,17 +274,60 @@ if _MCP_AVAILABLE and not _MCP_MESSAGE_HANDLER_SUPPORTED:
|
|
|
257
274
|
# Constants
|
|
258
275
|
# ---------------------------------------------------------------------------
|
|
259
276
|
|
|
260
|
-
_DEFAULT_TOOL_TIMEOUT =
|
|
277
|
+
_DEFAULT_TOOL_TIMEOUT = 300 # seconds for tool calls
|
|
261
278
|
_DEFAULT_CONNECT_TIMEOUT = 60 # seconds for initial connection per server
|
|
262
279
|
_MAX_RECONNECT_RETRIES = 5
|
|
263
280
|
_MAX_INITIAL_CONNECT_RETRIES = 3 # retries for the very first connection attempt
|
|
264
281
|
_MAX_BACKOFF_SECONDS = 60
|
|
265
282
|
|
|
283
|
+
# Keepalive cadence for HTTP/SSE sessions. The MCP spec lets a server expire
|
|
284
|
+
# idle sessions on any TTL it chooses (Streamable HTTP "Session Management"),
|
|
285
|
+
# so a client that wants a session to survive idle periods MUST refresh faster
|
|
286
|
+
# than that TTL. The default suits long LB/NAT idle windows (commonly
|
|
287
|
+
# 300-600s); servers with short session TTLs (e.g. Unreal Engine's editor MCP,
|
|
288
|
+
# ~15s) need a smaller ``keepalive_interval`` in their config or every idle
|
|
289
|
+
# tool call lands on a dead session and pays the full reconnect path. The floor
|
|
290
|
+
# stops a misconfigured tiny interval from busy-looping the keepalive.
|
|
291
|
+
_DEFAULT_KEEPALIVE_INTERVAL = 180 # seconds between liveness pings
|
|
292
|
+
_MIN_KEEPALIVE_INTERVAL = 5 # clamp floor for configured intervals
|
|
293
|
+
|
|
266
294
|
# Environment variables that are safe to pass to stdio subprocesses
|
|
267
295
|
_SAFE_ENV_KEYS = frozenset({
|
|
268
296
|
"PATH", "HOME", "USER", "LANG", "LC_ALL", "TERM", "SHELL", "TMPDIR",
|
|
269
297
|
})
|
|
270
298
|
|
|
299
|
+
_SAFE_ENV_KEYS_CASE_INSENSITIVE = frozenset({
|
|
300
|
+
# Windows process/location vars. These are needed by launcher-style tools
|
|
301
|
+
# such as Docker Desktop's MCP plugin discovery, and do not carry secrets.
|
|
302
|
+
"ALLUSERSPROFILE",
|
|
303
|
+
"APPDATA",
|
|
304
|
+
"COMMONPROGRAMFILES",
|
|
305
|
+
"COMMONPROGRAMFILES(X86)",
|
|
306
|
+
"COMMONPROGRAMW6432",
|
|
307
|
+
"COMPUTERNAME",
|
|
308
|
+
"COMSPEC",
|
|
309
|
+
"HOMEDRIVE",
|
|
310
|
+
"HOMEPATH",
|
|
311
|
+
"LOCALAPPDATA",
|
|
312
|
+
"NUMBER_OF_PROCESSORS",
|
|
313
|
+
"OS",
|
|
314
|
+
"PATHEXT",
|
|
315
|
+
"PROCESSOR_ARCHITECTURE",
|
|
316
|
+
"PROGRAMDATA",
|
|
317
|
+
"PROGRAMFILES",
|
|
318
|
+
"PROGRAMFILES(X86)",
|
|
319
|
+
"PROGRAMW6432",
|
|
320
|
+
"PUBLIC",
|
|
321
|
+
"SYSTEMDRIVE",
|
|
322
|
+
"SYSTEMROOT",
|
|
323
|
+
"TEMP",
|
|
324
|
+
"TMP",
|
|
325
|
+
"USERDOMAIN",
|
|
326
|
+
"USERNAME",
|
|
327
|
+
"USERPROFILE",
|
|
328
|
+
"WINDIR",
|
|
329
|
+
})
|
|
330
|
+
|
|
271
331
|
# Regex for credential patterns to strip from error messages
|
|
272
332
|
_CREDENTIAL_PATTERN = re.compile(
|
|
273
333
|
r"(?:"
|
|
@@ -305,7 +365,11 @@ def _build_safe_env(user_env: Optional[dict]) -> dict:
|
|
|
305
365
|
"""
|
|
306
366
|
env = {}
|
|
307
367
|
for key, value in os.environ.items():
|
|
308
|
-
if
|
|
368
|
+
if (
|
|
369
|
+
key in _SAFE_ENV_KEYS
|
|
370
|
+
or key.upper() in _SAFE_ENV_KEYS_CASE_INSENSITIVE
|
|
371
|
+
or key.startswith("XDG_")
|
|
372
|
+
):
|
|
309
373
|
env[key] = value
|
|
310
374
|
if user_env:
|
|
311
375
|
env.update(user_env)
|
|
@@ -333,6 +397,40 @@ def _exc_str(exc: BaseException) -> str:
|
|
|
333
397
|
return text if text else repr(exc)
|
|
334
398
|
|
|
335
399
|
|
|
400
|
+
# JSON-RPC "method not found" — the error a server returns when it does not
|
|
401
|
+
# implement a requested method (e.g. a tool-capable server that never wired up
|
|
402
|
+
# the optional ``ping`` utility). Defined locally with a fallback so detection
|
|
403
|
+
# works even on SDK builds that don't export the constant.
|
|
404
|
+
try:
|
|
405
|
+
from mcp.types import METHOD_NOT_FOUND as _JSONRPC_METHOD_NOT_FOUND
|
|
406
|
+
except Exception: # pragma: no cover — older/newer SDK without the constant
|
|
407
|
+
_JSONRPC_METHOD_NOT_FOUND = -32601
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _is_method_not_found_error(exc: BaseException) -> bool:
|
|
411
|
+
"""Return True if *exc* is a JSON-RPC ``method not found`` (-32601).
|
|
412
|
+
|
|
413
|
+
``ping`` is an *optional* MCP utility (spec: "optional ping mechanism").
|
|
414
|
+
A server that doesn't implement it answers a ping with -32601 rather than
|
|
415
|
+
an empty result. Structurally inspect ``McpError.error.code`` first, then
|
|
416
|
+
fall back to a substring match so detection survives SDK version drift and
|
|
417
|
+
servers that surface the condition as a plain message.
|
|
418
|
+
"""
|
|
419
|
+
# Structural: mcp.shared.exceptions.McpError carries ErrorData.code.
|
|
420
|
+
err = getattr(exc, "error", None)
|
|
421
|
+
code = getattr(err, "code", None)
|
|
422
|
+
if code == _JSONRPC_METHOD_NOT_FOUND:
|
|
423
|
+
return True
|
|
424
|
+
msg = str(exc).lower()
|
|
425
|
+
if not msg:
|
|
426
|
+
return False
|
|
427
|
+
return (
|
|
428
|
+
str(_JSONRPC_METHOD_NOT_FOUND) in msg
|
|
429
|
+
or "method not found" in msg
|
|
430
|
+
or "not found: ping" in msg
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
336
434
|
# ---------------------------------------------------------------------------
|
|
337
435
|
# MCP tool description content scanning
|
|
338
436
|
# ---------------------------------------------------------------------------
|
|
@@ -1104,6 +1202,193 @@ class SamplingHandler:
|
|
|
1104
1202
|
return self._build_text_result(choice, response)
|
|
1105
1203
|
|
|
1106
1204
|
|
|
1205
|
+
# ---------------------------------------------------------------------------
|
|
1206
|
+
# Elicitation handler
|
|
1207
|
+
# ---------------------------------------------------------------------------
|
|
1208
|
+
|
|
1209
|
+
def _format_elicitation_schema_summary(schema: dict, server_name: str) -> str:
|
|
1210
|
+
"""Render a JSON-schema-ish requested_schema to a human-readable field list.
|
|
1211
|
+
|
|
1212
|
+
Elicitation schemas are restricted to a flat object with named top-level
|
|
1213
|
+
properties. We surface field names, types, and descriptions so the user
|
|
1214
|
+
can tell what the server is asking for before approving.
|
|
1215
|
+
"""
|
|
1216
|
+
props = schema.get("properties") if isinstance(schema, dict) else None
|
|
1217
|
+
if not isinstance(props, dict) or not props:
|
|
1218
|
+
return f"Approval requested by MCP server '{server_name}'."
|
|
1219
|
+
|
|
1220
|
+
lines = [f"Fields requested by MCP server '{server_name}':"]
|
|
1221
|
+
for field_name, field_spec in props.items():
|
|
1222
|
+
field_type = ""
|
|
1223
|
+
field_desc = ""
|
|
1224
|
+
if isinstance(field_spec, dict):
|
|
1225
|
+
field_type = str(field_spec.get("type", "") or "")
|
|
1226
|
+
field_desc = str(field_spec.get("description", "") or "")
|
|
1227
|
+
suffix = f" ({field_type})" if field_type else ""
|
|
1228
|
+
if field_desc:
|
|
1229
|
+
lines.append(f" - {field_name}{suffix}: {field_desc}")
|
|
1230
|
+
else:
|
|
1231
|
+
lines.append(f" - {field_name}{suffix}")
|
|
1232
|
+
return "\n".join(lines)
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class ElicitationHandler:
|
|
1236
|
+
"""Handles ``elicitation/create`` requests for a single MCP server.
|
|
1237
|
+
|
|
1238
|
+
Each ``MCPServerTask`` that has elicitation enabled creates one handler.
|
|
1239
|
+
The handler is callable and passed directly to ``ClientSession`` as the
|
|
1240
|
+
``elicitation_callback`` (added in mcp Python SDK 1.11.0).
|
|
1241
|
+
|
|
1242
|
+
Elicitation lets a server ask the client to collect structured input from
|
|
1243
|
+
the user mid-tool-call (e.g. payment authorization, OAuth confirmation).
|
|
1244
|
+
Form-mode elicitations are routed through Hermes' existing approval
|
|
1245
|
+
system (``tools.approval.prompt_dangerous_approval``), which surfaces
|
|
1246
|
+
the prompt on whichever surface the active session uses -- CLI, TUI,
|
|
1247
|
+
Telegram, Slack, etc. URL-mode elicitations are declined as unsupported.
|
|
1248
|
+
|
|
1249
|
+
Failure modes are fail-closed: any timeout, exception, or unexpected
|
|
1250
|
+
state returns ``decline``/``cancel`` rather than silently accepting.
|
|
1251
|
+
The server treats this as the user not approving.
|
|
1252
|
+
"""
|
|
1253
|
+
|
|
1254
|
+
# Outer cap for the approval await. ``prompt_dangerous_approval`` runs
|
|
1255
|
+
# its own input() timeout via the approval-config value; this is an
|
|
1256
|
+
# asyncio-side safety net so the MCP event loop never blocks
|
|
1257
|
+
# indefinitely if the inner timeout machinery is bypassed.
|
|
1258
|
+
_OUTER_TIMEOUT_GRACE_SECONDS = 5
|
|
1259
|
+
|
|
1260
|
+
def __init__(self, server_name: str, config: dict, owner: Optional["MCPServerTask"] = None):
|
|
1261
|
+
self.server_name = server_name
|
|
1262
|
+
# Per-elicitation timeout. Default 5 min mirrors the gateway approval
|
|
1263
|
+
# default so users on async surfaces (Telegram, Slack) have time to
|
|
1264
|
+
# respond before the server gives up.
|
|
1265
|
+
self.timeout = _safe_numeric(config.get("timeout", 300), 300, float)
|
|
1266
|
+
# Back-reference to the MCPServerTask so we can read the agent's
|
|
1267
|
+
# captured contextvars snapshot at elicitation time. Optional so
|
|
1268
|
+
# the handler stays unit-testable in isolation.
|
|
1269
|
+
self.owner = owner
|
|
1270
|
+
self.metrics = {
|
|
1271
|
+
"requests": 0,
|
|
1272
|
+
"accepted": 0,
|
|
1273
|
+
"declined": 0,
|
|
1274
|
+
"errors": 0,
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
def session_kwargs(self) -> dict:
|
|
1278
|
+
"""Return kwargs to pass to ClientSession for elicitation support."""
|
|
1279
|
+
return {"elicitation_callback": self}
|
|
1280
|
+
|
|
1281
|
+
async def __call__(self, context, params):
|
|
1282
|
+
"""Elicitation callback invoked by the MCP SDK.
|
|
1283
|
+
|
|
1284
|
+
Conforms to ``ElicitationFnT`` protocol. Returns ``ElicitResult``
|
|
1285
|
+
or ``ErrorData``.
|
|
1286
|
+
"""
|
|
1287
|
+
self.metrics["requests"] += 1
|
|
1288
|
+
|
|
1289
|
+
# URL-mode elicitations point the user to an external URL for
|
|
1290
|
+
# sensitive out-of-band flows (OAuth, payment processing). Honouring
|
|
1291
|
+
# them requires opening a browser to that URL and waiting for the
|
|
1292
|
+
# server's notifications/elicitation/complete -- out of scope for
|
|
1293
|
+
# the initial implementation. Decline cleanly so the server does
|
|
1294
|
+
# not hang.
|
|
1295
|
+
mode = getattr(params, "mode", "form")
|
|
1296
|
+
if mode == "url":
|
|
1297
|
+
logger.info(
|
|
1298
|
+
"MCP server '%s' requested URL-mode elicitation; "
|
|
1299
|
+
"declining (URL-mode elicitation not implemented)",
|
|
1300
|
+
self.server_name,
|
|
1301
|
+
)
|
|
1302
|
+
self.metrics["declined"] += 1
|
|
1303
|
+
return ElicitResult(action="decline")
|
|
1304
|
+
|
|
1305
|
+
message = getattr(params, "message", "") or (
|
|
1306
|
+
f"MCP server '{self.server_name}' is requesting your approval"
|
|
1307
|
+
)
|
|
1308
|
+
schema = getattr(params, "requested_schema", {}) or {}
|
|
1309
|
+
description = _format_elicitation_schema_summary(schema, self.server_name)
|
|
1310
|
+
|
|
1311
|
+
logger.info(
|
|
1312
|
+
"MCP server '%s' elicitation request: %s",
|
|
1313
|
+
self.server_name, _sanitize_error(message)[:200],
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
# Lazy import: tools.approval is imported very early during process
|
|
1317
|
+
# bootstrap; matching the lazy pattern used by _fire_approval_hook
|
|
1318
|
+
# avoids any chance of import-order coupling.
|
|
1319
|
+
try:
|
|
1320
|
+
from tools.approval import request_elicitation_consent
|
|
1321
|
+
except Exception as exc: # pragma: no cover -- defensive
|
|
1322
|
+
logger.error(
|
|
1323
|
+
"MCP server '%s' elicitation: approval system unavailable: %s",
|
|
1324
|
+
self.server_name, exc,
|
|
1325
|
+
)
|
|
1326
|
+
self.metrics["errors"] += 1
|
|
1327
|
+
return ElicitResult(action="decline")
|
|
1328
|
+
|
|
1329
|
+
# Offload the sync consent flow to a worker thread. Running it
|
|
1330
|
+
# inline would freeze the MCP background event loop, blocking every
|
|
1331
|
+
# other RPC on this session. request_elicitation_consent() routes
|
|
1332
|
+
# itself to the right surface (gateway notify_cb for Telegram /
|
|
1333
|
+
# Slack / etc., prompt_dangerous_approval for CLI / TUI) and
|
|
1334
|
+
# normalizes the answer to one of accept / decline / cancel.
|
|
1335
|
+
#
|
|
1336
|
+
# The recv-loop task that fires this callback does NOT inherit
|
|
1337
|
+
# the agent's contextvars (HERMES_SESSION_PLATFORM etc.). When
|
|
1338
|
+
# the MCP tool wrapper captured the agent's context onto
|
|
1339
|
+
# owner._pending_call_context we replay it here via
|
|
1340
|
+
# contextvars.Context.run so the gateway-platform detection in
|
|
1341
|
+
# request_elicitation_consent picks up the right session.
|
|
1342
|
+
captured = getattr(self.owner, "_pending_call_context", None) if self.owner else None
|
|
1343
|
+
|
|
1344
|
+
def _invoke_consent() -> str:
|
|
1345
|
+
if captured is None:
|
|
1346
|
+
return request_elicitation_consent(
|
|
1347
|
+
message,
|
|
1348
|
+
description,
|
|
1349
|
+
timeout_seconds=int(self.timeout),
|
|
1350
|
+
surface=f"mcp-elicitation/{self.server_name}",
|
|
1351
|
+
)
|
|
1352
|
+
# Context.run can only execute a context once — copy to allow
|
|
1353
|
+
# multiple elicitations within a single tool call.
|
|
1354
|
+
return captured.copy().run(
|
|
1355
|
+
request_elicitation_consent,
|
|
1356
|
+
message,
|
|
1357
|
+
description,
|
|
1358
|
+
timeout_seconds=int(self.timeout),
|
|
1359
|
+
surface=f"mcp-elicitation/{self.server_name}",
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
try:
|
|
1363
|
+
answer = await asyncio.wait_for(
|
|
1364
|
+
asyncio.to_thread(_invoke_consent),
|
|
1365
|
+
timeout=self.timeout + self._OUTER_TIMEOUT_GRACE_SECONDS,
|
|
1366
|
+
)
|
|
1367
|
+
except asyncio.TimeoutError:
|
|
1368
|
+
logger.warning(
|
|
1369
|
+
"MCP server '%s' elicitation timed out after %ds",
|
|
1370
|
+
self.server_name, int(self.timeout),
|
|
1371
|
+
)
|
|
1372
|
+
self.metrics["errors"] += 1
|
|
1373
|
+
return ElicitResult(action="cancel")
|
|
1374
|
+
except Exception as exc:
|
|
1375
|
+
logger.error(
|
|
1376
|
+
"MCP server '%s' elicitation failed: %s",
|
|
1377
|
+
self.server_name, exc, exc_info=True,
|
|
1378
|
+
)
|
|
1379
|
+
self.metrics["errors"] += 1
|
|
1380
|
+
return ElicitResult(action="decline")
|
|
1381
|
+
|
|
1382
|
+
if answer == "accept":
|
|
1383
|
+
self.metrics["accepted"] += 1
|
|
1384
|
+
return ElicitResult(action="accept", content={})
|
|
1385
|
+
if answer == "cancel":
|
|
1386
|
+
self.metrics["errors"] += 1
|
|
1387
|
+
return ElicitResult(action="cancel")
|
|
1388
|
+
self.metrics["declined"] += 1
|
|
1389
|
+
return ElicitResult(action="decline")
|
|
1390
|
+
|
|
1391
|
+
|
|
1107
1392
|
# ---------------------------------------------------------------------------
|
|
1108
1393
|
# Server task -- each MCP server lives in one long-lived asyncio Task
|
|
1109
1394
|
# ---------------------------------------------------------------------------
|
|
@@ -1122,9 +1407,11 @@ class MCPServerTask:
|
|
|
1122
1407
|
"name", "session", "tool_timeout",
|
|
1123
1408
|
"_task", "_ready", "_shutdown_event", "_reconnect_event",
|
|
1124
1409
|
"_tools", "_error", "_config",
|
|
1125
|
-
"_sampling", "
|
|
1410
|
+
"_sampling", "_elicitation",
|
|
1411
|
+
"_registered_tool_names", "_auth_type", "_refresh_lock",
|
|
1126
1412
|
"_rpc_lock", "_pending_refresh_tasks",
|
|
1127
|
-
"
|
|
1413
|
+
"_pending_call_context",
|
|
1414
|
+
"initialize_result", "_ping_unsupported",
|
|
1128
1415
|
)
|
|
1129
1416
|
|
|
1130
1417
|
def __init__(self, name: str):
|
|
@@ -1144,6 +1431,7 @@ class MCPServerTask:
|
|
|
1144
1431
|
self._error: Optional[Exception] = None
|
|
1145
1432
|
self._config: dict = {}
|
|
1146
1433
|
self._sampling: Optional[SamplingHandler] = None
|
|
1434
|
+
self._elicitation: Optional[ElicitationHandler] = None
|
|
1147
1435
|
self._registered_tool_names: list[str] = []
|
|
1148
1436
|
self._auth_type: str = ""
|
|
1149
1437
|
self._refresh_lock = asyncio.Lock()
|
|
@@ -1155,17 +1443,53 @@ class MCPServerTask:
|
|
|
1155
1443
|
# transports for conservative per-server ordering.
|
|
1156
1444
|
self._rpc_lock = asyncio.Lock()
|
|
1157
1445
|
self._pending_refresh_tasks: set[asyncio.Task] = set()
|
|
1446
|
+
# contextvars snapshot of the agent task that's currently in
|
|
1447
|
+
# session.call_tool(). The MCP recv loop dispatches incoming
|
|
1448
|
+
# elicitation/create requests on a SEPARATE asyncio task whose
|
|
1449
|
+
# context doesn't inherit HERMES_SESSION_PLATFORM, so the
|
|
1450
|
+
# elicitation handler has no way to detect the gateway session
|
|
1451
|
+
# that triggered the call. Capturing the agent's context here
|
|
1452
|
+
# and replaying it inside the elicitation callback restores
|
|
1453
|
+
# gateway-platform attribution and routes the approval prompt
|
|
1454
|
+
# to the right surface (Telegram, Slack, etc.).
|
|
1455
|
+
self._pending_call_context: Optional[contextvars.Context] = None
|
|
1158
1456
|
# Captures the ``InitializeResult`` returned by
|
|
1159
1457
|
# ``await session.initialize()`` so downstream code can inspect the
|
|
1160
1458
|
# server's real advertised capabilities (``.capabilities.resources``,
|
|
1161
1459
|
# ``.capabilities.prompts``) instead of assuming every ``ClientSession``
|
|
1162
1460
|
# method attribute corresponds to a supported server method. See #18051.
|
|
1163
1461
|
self.initialize_result: Optional[Any] = None
|
|
1462
|
+
# Set True the first time a keepalive ``ping`` returns JSON-RPC
|
|
1463
|
+
# -32601 (method not found): the server is tool-capable but doesn't
|
|
1464
|
+
# implement the optional ``ping`` utility. Subsequent keepalives fall
|
|
1465
|
+
# back to ``list_tools`` (the pre-ping probe) so we neither spam pings
|
|
1466
|
+
# nor reconnect-loop. Reset on each fresh transport connection.
|
|
1467
|
+
self._ping_unsupported: bool = False
|
|
1164
1468
|
|
|
1165
1469
|
def _is_http(self) -> bool:
|
|
1166
1470
|
"""Check if this server uses HTTP transport."""
|
|
1167
1471
|
return "url" in self._config
|
|
1168
1472
|
|
|
1473
|
+
def _advertises_tools(self) -> bool:
|
|
1474
|
+
"""Whether the server advertises the ``tools`` capability.
|
|
1475
|
+
|
|
1476
|
+
Per the MCP spec, ``InitializeResult.capabilities.tools`` is non-None
|
|
1477
|
+
iff the server implements the ``tools/*`` request family. Prompt-only
|
|
1478
|
+
or resource-only servers omit it, and calling ``tools/list`` against
|
|
1479
|
+
them raises ``McpError(-32601 Method not found)`` — which previously
|
|
1480
|
+
killed the connection during discovery and made every keepalive fail.
|
|
1481
|
+
(Ported from anomalyco/opencode#31271.)
|
|
1482
|
+
|
|
1483
|
+
Returns True when no capability info was captured (legacy fallback:
|
|
1484
|
+
preserve the old always-call-list_tools behavior rather than regress
|
|
1485
|
+
any server that was working before this gate).
|
|
1486
|
+
"""
|
|
1487
|
+
init_result = self.initialize_result
|
|
1488
|
+
caps = getattr(init_result, "capabilities", None) if init_result is not None else None
|
|
1489
|
+
if caps is None:
|
|
1490
|
+
return True
|
|
1491
|
+
return getattr(caps, "tools", None) is not None
|
|
1492
|
+
|
|
1169
1493
|
# ----- Dynamic tool discovery (notifications/tools/list_changed) -----
|
|
1170
1494
|
|
|
1171
1495
|
async def _refresh_tools_task(self):
|
|
@@ -1237,6 +1561,12 @@ class MCPServerTask:
|
|
|
1237
1561
|
"""
|
|
1238
1562
|
from tools.registry import registry
|
|
1239
1563
|
|
|
1564
|
+
if not self._advertises_tools():
|
|
1565
|
+
# A server that doesn't implement tools/* should never send
|
|
1566
|
+
# tools/list_changed, but guard anyway — calling tools/list
|
|
1567
|
+
# would raise McpError(-32601).
|
|
1568
|
+
return
|
|
1569
|
+
|
|
1240
1570
|
async with self._refresh_lock:
|
|
1241
1571
|
# Capture old tool names for change diff
|
|
1242
1572
|
old_tool_names = set(self._registered_tool_names)
|
|
@@ -1289,6 +1619,46 @@ class MCPServerTask:
|
|
|
1289
1619
|
self.name, len(self._registered_tool_names),
|
|
1290
1620
|
)
|
|
1291
1621
|
|
|
1622
|
+
async def _keepalive_probe(self) -> None:
|
|
1623
|
+
"""Exercise the session to detect a stale/expired connection.
|
|
1624
|
+
|
|
1625
|
+
Uses ``ping`` (cheap, transport-agnostic liveness) by default. ``ping``
|
|
1626
|
+
is an OPTIONAL MCP utility: a server that doesn't implement it answers
|
|
1627
|
+
JSON-RPC -32601. The first time that happens we latch
|
|
1628
|
+
``_ping_unsupported`` and fall back to the pre-ping probe — capability
|
|
1629
|
+
permitting, ``list_tools``; otherwise ``ping`` is the only option and
|
|
1630
|
+
the -32601 propagates (a server advertising neither a working ping nor
|
|
1631
|
+
tools has no liveness primitive left). The latch resets on each fresh
|
|
1632
|
+
transport connection so a server that gains ping support after a
|
|
1633
|
+
reconnect is re-probed with the cheap path.
|
|
1634
|
+
|
|
1635
|
+
Raises on a genuine connection failure so the caller triggers a
|
|
1636
|
+
reconnect; returns normally when the session is alive.
|
|
1637
|
+
"""
|
|
1638
|
+
if not self._ping_unsupported:
|
|
1639
|
+
try:
|
|
1640
|
+
await asyncio.wait_for(self.session.send_ping(), timeout=30.0)
|
|
1641
|
+
return
|
|
1642
|
+
except Exception as exc:
|
|
1643
|
+
# Only a "method not found" means ping is unsupported. Any
|
|
1644
|
+
# other error (timeout, closed transport, session expired) is
|
|
1645
|
+
# a real liveness failure — propagate so we reconnect.
|
|
1646
|
+
if not _is_method_not_found_error(exc):
|
|
1647
|
+
raise
|
|
1648
|
+
if not self._advertises_tools():
|
|
1649
|
+
# No ping, no tools → no cheaper probe to fall back to.
|
|
1650
|
+
raise
|
|
1651
|
+
self._ping_unsupported = True
|
|
1652
|
+
logger.info(
|
|
1653
|
+
"MCP server '%s': does not implement the optional 'ping' "
|
|
1654
|
+
"utility (-32601); using 'list_tools' for keepalive on "
|
|
1655
|
+
"this connection.",
|
|
1656
|
+
self.name,
|
|
1657
|
+
)
|
|
1658
|
+
|
|
1659
|
+
# Fallback probe for servers without ping support.
|
|
1660
|
+
await asyncio.wait_for(self.session.list_tools(), timeout=30.0)
|
|
1661
|
+
|
|
1292
1662
|
async def _wait_for_lifecycle_event(self) -> str:
|
|
1293
1663
|
"""Block until either _shutdown_event or _reconnect_event fires.
|
|
1294
1664
|
|
|
@@ -1302,13 +1672,29 @@ class MCPServerTask:
|
|
|
1302
1672
|
|
|
1303
1673
|
Shutdown takes precedence if both events are set simultaneously.
|
|
1304
1674
|
|
|
1305
|
-
Periodically sends a lightweight keepalive (``
|
|
1306
|
-
|
|
1307
|
-
|
|
1675
|
+
Periodically sends a lightweight keepalive (``ping``, with a
|
|
1676
|
+
``list_tools`` fallback for servers that don't implement the optional
|
|
1677
|
+
ping utility — see :meth:`_keepalive_probe`) to prevent TCP/session
|
|
1678
|
+
state from going stale during idle periods (#17003). If the keepalive
|
|
1679
|
+
fails, triggers a reconnect.
|
|
1680
|
+
|
|
1681
|
+
The cadence is ``keepalive_interval`` from server config (default
|
|
1682
|
+
:data:`_DEFAULT_KEEPALIVE_INTERVAL`, floored at
|
|
1683
|
+
:data:`_MIN_KEEPALIVE_INTERVAL`). Servers that GC idle sessions on a
|
|
1684
|
+
short TTL (e.g. Unreal Engine's editor MCP, ~15s) need an interval
|
|
1685
|
+
below that TTL, otherwise every idle tool call lands on an
|
|
1686
|
+
already-expired session and pays the full reconnect path.
|
|
1308
1687
|
"""
|
|
1309
|
-
#
|
|
1310
|
-
#
|
|
1311
|
-
|
|
1688
|
+
# Refresh faster than the server's session TTL. ``ping`` (MCP base
|
|
1689
|
+
# protocol liveness) is used rather than ``list_tools`` so the probe
|
|
1690
|
+
# stays a few bytes regardless of how many tools the server exposes —
|
|
1691
|
+
# a ``list_tools`` keepalive against an 830-tool server would pull
|
|
1692
|
+
# ~1 MB every cycle. Tool-list changes still arrive out-of-band via
|
|
1693
|
+
# ``notifications/tools/list_changed`` → ``_refresh_tools``.
|
|
1694
|
+
keepalive_interval = max(
|
|
1695
|
+
_MIN_KEEPALIVE_INTERVAL,
|
|
1696
|
+
float(self._config.get("keepalive_interval", _DEFAULT_KEEPALIVE_INTERVAL)),
|
|
1697
|
+
)
|
|
1312
1698
|
|
|
1313
1699
|
shutdown_task = asyncio.create_task(self._shutdown_event.wait())
|
|
1314
1700
|
reconnect_task = asyncio.create_task(self._reconnect_event.wait())
|
|
@@ -1316,20 +1702,23 @@ class MCPServerTask:
|
|
|
1316
1702
|
while True:
|
|
1317
1703
|
done, _pending = await asyncio.wait(
|
|
1318
1704
|
{shutdown_task, reconnect_task},
|
|
1319
|
-
timeout=
|
|
1705
|
+
timeout=keepalive_interval,
|
|
1320
1706
|
return_when=asyncio.FIRST_COMPLETED,
|
|
1321
1707
|
)
|
|
1322
1708
|
if done:
|
|
1323
1709
|
break
|
|
1324
1710
|
|
|
1325
|
-
# Timeout — no lifecycle event fired.
|
|
1326
|
-
# to
|
|
1711
|
+
# Timeout — no lifecycle event fired. Probe the connection
|
|
1712
|
+
# to detect stale/expired sessions. Prefer ``ping`` (MCP base
|
|
1713
|
+
# protocol liveness): it works uniformly and stays a few bytes
|
|
1714
|
+
# regardless of tool count, unlike ``list_tools`` (~1 MB on an
|
|
1715
|
+
# 830-tool server). ``ping`` is an OPTIONAL utility, so a
|
|
1716
|
+
# tool-capable server that doesn't implement it answers -32601;
|
|
1717
|
+
# in that case fall back to the pre-ping ``list_tools`` probe
|
|
1718
|
+
# for the rest of this connection rather than reconnect-looping.
|
|
1327
1719
|
if self.session:
|
|
1328
1720
|
try:
|
|
1329
|
-
await
|
|
1330
|
-
self.session.list_tools(),
|
|
1331
|
-
timeout=30.0,
|
|
1332
|
-
)
|
|
1721
|
+
await self._keepalive_probe()
|
|
1333
1722
|
except Exception as exc:
|
|
1334
1723
|
logger.warning(
|
|
1335
1724
|
"MCP server '%s' keepalive failed, "
|
|
@@ -1390,6 +1779,8 @@ class MCPServerTask:
|
|
|
1390
1779
|
)
|
|
1391
1780
|
|
|
1392
1781
|
sampling_kwargs = self._sampling.session_kwargs() if self._sampling else {}
|
|
1782
|
+
if self._elicitation:
|
|
1783
|
+
sampling_kwargs.update(self._elicitation.session_kwargs())
|
|
1393
1784
|
if _MCP_NOTIFICATION_TYPES and _MCP_MESSAGE_HANDLER_SUPPORTED:
|
|
1394
1785
|
sampling_kwargs["message_handler"] = self._make_message_handler()
|
|
1395
1786
|
|
|
@@ -1591,6 +1982,8 @@ class MCPServerTask:
|
|
|
1591
1982
|
raise
|
|
1592
1983
|
|
|
1593
1984
|
sampling_kwargs = self._sampling.session_kwargs() if self._sampling else {}
|
|
1985
|
+
if self._elicitation:
|
|
1986
|
+
sampling_kwargs.update(self._elicitation.session_kwargs())
|
|
1594
1987
|
if _MCP_NOTIFICATION_TYPES and _MCP_MESSAGE_HANDLER_SUPPORTED:
|
|
1595
1988
|
sampling_kwargs["message_handler"] = self._make_message_handler()
|
|
1596
1989
|
|
|
@@ -1742,9 +2135,29 @@ class MCPServerTask:
|
|
|
1742
2135
|
)
|
|
1743
2136
|
|
|
1744
2137
|
async def _discover_tools(self):
|
|
1745
|
-
"""Discover tools from the connected session.
|
|
2138
|
+
"""Discover tools from the connected session.
|
|
2139
|
+
|
|
2140
|
+
Capability-gated: prompt-only / resource-only MCP servers don't
|
|
2141
|
+
implement ``tools/list``, and calling it raises ``McpError(-32601)``,
|
|
2142
|
+
which previously aborted the connection — those servers could never
|
|
2143
|
+
stay connected for their prompts/resources. Skip the call when the
|
|
2144
|
+
server doesn't advertise the ``tools`` capability.
|
|
2145
|
+
(Ported from anomalyco/opencode#31271.)
|
|
2146
|
+
"""
|
|
2147
|
+
# Fresh transport connection → re-probe with the cheap ``ping`` path.
|
|
2148
|
+
# Clears any latch from a prior connection in case the server gained
|
|
2149
|
+
# ping support across the reconnect.
|
|
2150
|
+
self._ping_unsupported = False
|
|
1746
2151
|
if self.session is None:
|
|
1747
2152
|
return
|
|
2153
|
+
if not self._advertises_tools():
|
|
2154
|
+
logger.info(
|
|
2155
|
+
"MCP server '%s': does not advertise 'tools' capability — "
|
|
2156
|
+
"skipping tools/list (prompts/resources remain available)",
|
|
2157
|
+
self.name,
|
|
2158
|
+
)
|
|
2159
|
+
self._tools = []
|
|
2160
|
+
return
|
|
1748
2161
|
async with self._rpc_lock:
|
|
1749
2162
|
tools_result = await self.session.list_tools()
|
|
1750
2163
|
self._tools = (
|
|
@@ -1770,6 +2183,16 @@ class MCPServerTask:
|
|
|
1770
2183
|
else:
|
|
1771
2184
|
self._sampling = None
|
|
1772
2185
|
|
|
2186
|
+
# Set up elicitation handler if enabled and SDK types are available.
|
|
2187
|
+
# Servers use elicitation/create to ask the client for structured
|
|
2188
|
+
# input mid-tool-call (e.g. payment authorization). The handler
|
|
2189
|
+
# routes those requests through Hermes' approval system.
|
|
2190
|
+
elicitation_config = config.get("elicitation", {})
|
|
2191
|
+
if elicitation_config.get("enabled", True) and _MCP_ELICITATION_TYPES:
|
|
2192
|
+
self._elicitation = ElicitationHandler(self.name, elicitation_config, owner=self)
|
|
2193
|
+
else:
|
|
2194
|
+
self._elicitation = None
|
|
2195
|
+
|
|
1773
2196
|
# Validate: warn if both url and command are present
|
|
1774
2197
|
if "url" in config and "command" in config:
|
|
1775
2198
|
logger.warning(
|
|
@@ -1800,7 +2223,12 @@ class MCPServerTask:
|
|
|
1800
2223
|
# before surfacing an opaque CancelledError. Probing here — once,
|
|
1801
2224
|
# outside the SDK task group — fails fast and non-retryably with
|
|
1802
2225
|
# an actionable message, mirroring the URL-validation path above.
|
|
1803
|
-
|
|
2226
|
+
# Skip the probe when _ready is already set: that only happens
|
|
2227
|
+
# after a prior successful connect, so this run() invocation is a
|
|
2228
|
+
# reconnect (OAuth recovery / manual refresh). The endpoint was
|
|
2229
|
+
# already validated once; re-probing burns a redundant network
|
|
2230
|
+
# round-trip against a known-good server on every reconnect.
|
|
2231
|
+
if config.get("transport") != "sse" and not self._ready.is_set():
|
|
1804
2232
|
try:
|
|
1805
2233
|
_probe_headers = dict(config.get("headers") or {})
|
|
1806
2234
|
await self._preflight_content_type(
|
|
@@ -1981,6 +2409,8 @@ class MCPServerTask:
|
|
|
1981
2409
|
# ---------------------------------------------------------------------------
|
|
1982
2410
|
|
|
1983
2411
|
_servers: Dict[str, MCPServerTask] = {}
|
|
2412
|
+
_server_connecting: set[str] = set()
|
|
2413
|
+
_server_connect_errors: Dict[str, str] = {}
|
|
1984
2414
|
|
|
1985
2415
|
# Circuit breaker: consecutive error counts per server. After
|
|
1986
2416
|
# _CIRCUIT_BREAKER_THRESHOLD consecutive failures, the handler returns
|
|
@@ -2367,8 +2797,8 @@ _mcp_tool_server_names: Dict[str, str] = {}
|
|
|
2367
2797
|
_mcp_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
2368
2798
|
_mcp_thread: Optional[threading.Thread] = None
|
|
2369
2799
|
|
|
2370
|
-
# Protects _mcp_loop, _mcp_thread, _servers,
|
|
2371
|
-
# _mcp_tool_server_names, and _stdio_pids.
|
|
2800
|
+
# Protects _mcp_loop, _mcp_thread, _servers, MCP connection status maps,
|
|
2801
|
+
# _parallel_safe_servers, _mcp_tool_server_names, and _stdio_pids.
|
|
2372
2802
|
_lock = threading.Lock()
|
|
2373
2803
|
|
|
2374
2804
|
# PIDs of stdio MCP server subprocesses. Tracked so we can force-kill
|
|
@@ -2455,6 +2885,37 @@ def _ensure_mcp_loop():
|
|
|
2455
2885
|
_mcp_thread.start()
|
|
2456
2886
|
|
|
2457
2887
|
|
|
2888
|
+
def _wrap_with_home_override(coro: "Coroutine") -> "Coroutine":
|
|
2889
|
+
"""Carry the caller's context-local HERMES_HOME override into ``coro``.
|
|
2890
|
+
|
|
2891
|
+
Returns ``coro`` unchanged when no override is active. Otherwise wraps
|
|
2892
|
+
it so the override is set inside the coroutine's own (task-local)
|
|
2893
|
+
context on the MCP loop and reset when it completes — concurrent calls
|
|
2894
|
+
carrying different scopes don't interfere.
|
|
2895
|
+
"""
|
|
2896
|
+
try:
|
|
2897
|
+
from hermes_constants import (
|
|
2898
|
+
get_hermes_home_override,
|
|
2899
|
+
reset_hermes_home_override,
|
|
2900
|
+
set_hermes_home_override,
|
|
2901
|
+
)
|
|
2902
|
+
|
|
2903
|
+
home_override = get_hermes_home_override()
|
|
2904
|
+
except Exception:
|
|
2905
|
+
return coro
|
|
2906
|
+
if not home_override:
|
|
2907
|
+
return coro
|
|
2908
|
+
|
|
2909
|
+
async def _scoped():
|
|
2910
|
+
token = set_hermes_home_override(home_override)
|
|
2911
|
+
try:
|
|
2912
|
+
return await coro
|
|
2913
|
+
finally:
|
|
2914
|
+
reset_hermes_home_override(token)
|
|
2915
|
+
|
|
2916
|
+
return _scoped()
|
|
2917
|
+
|
|
2918
|
+
|
|
2458
2919
|
def _run_on_mcp_loop(coro_or_factory, timeout: float = 30):
|
|
2459
2920
|
"""Schedule a coroutine on the MCP event loop and block until done.
|
|
2460
2921
|
|
|
@@ -2477,6 +2938,19 @@ def _run_on_mcp_loop(coro_or_factory, timeout: float = 30):
|
|
|
2477
2938
|
raise RuntimeError("MCP event loop is not running")
|
|
2478
2939
|
|
|
2479
2940
|
coro = coro_or_factory() if callable(coro_or_factory) else coro_or_factory
|
|
2941
|
+
|
|
2942
|
+
# Propagate the context-local HERMES_HOME override onto the MCP loop.
|
|
2943
|
+
# Tasks scheduled via run_coroutine_threadsafe are created INSIDE the
|
|
2944
|
+
# loop thread, so they copy the loop thread's context — not the
|
|
2945
|
+
# scheduling thread's. A per-request profile scope (the dashboard's
|
|
2946
|
+
# ?profile= endpoints, e.g. the MCP "Test server" probe) would silently
|
|
2947
|
+
# vanish here: OAuth token stores and any other get_hermes_home()
|
|
2948
|
+
# resolution inside the coroutine would read the process home instead
|
|
2949
|
+
# of the selected profile's. Re-establish the override inside the
|
|
2950
|
+
# task's own context (task-local — concurrent calls carrying different
|
|
2951
|
+
# scopes don't interfere). No-op when no override is active.
|
|
2952
|
+
coro = _wrap_with_home_override(coro)
|
|
2953
|
+
|
|
2480
2954
|
future = safe_schedule_threadsafe(
|
|
2481
2955
|
coro, loop,
|
|
2482
2956
|
logger=logger,
|
|
@@ -2522,10 +2996,19 @@ def _interrupted_call_result() -> str:
|
|
|
2522
2996
|
# ---------------------------------------------------------------------------
|
|
2523
2997
|
|
|
2524
2998
|
def _interpolate_env_vars(value):
|
|
2525
|
-
"""Recursively resolve ``${VAR}`` placeholders
|
|
2999
|
+
"""Recursively resolve ``${VAR}`` placeholders.
|
|
3000
|
+
|
|
3001
|
+
Resolves from the active profile's secret scope when multiplexing is on
|
|
3002
|
+
(so an MCP server config's ``${API_KEY}`` picks up the routed profile's
|
|
3003
|
+
value, not the process-global ``os.environ`` which may hold another
|
|
3004
|
+
profile's), falling back to ``os.environ`` otherwise. Unset vars keep the
|
|
3005
|
+
literal ``${VAR}`` placeholder, as before.
|
|
3006
|
+
"""
|
|
3007
|
+
from agent.secret_scope import get_secret as _get_secret
|
|
3008
|
+
|
|
2526
3009
|
if isinstance(value, str):
|
|
2527
3010
|
def _replace(m):
|
|
2528
|
-
return
|
|
3011
|
+
return _get_secret(m.group(1), m.group(0)) or m.group(0)
|
|
2529
3012
|
return _ENV_VAR_PATTERN.sub(_replace, value)
|
|
2530
3013
|
if isinstance(value, dict):
|
|
2531
3014
|
return {k: _interpolate_env_vars(v) for k, v in value.items()}
|
|
@@ -2534,6 +3017,33 @@ def _interpolate_env_vars(value):
|
|
|
2534
3017
|
return value
|
|
2535
3018
|
|
|
2536
3019
|
|
|
3020
|
+
def _filter_suspicious_mcp_servers(servers: Dict[str, dict]) -> Dict[str, dict]:
|
|
3021
|
+
"""Drop exfiltration-shaped MCP configs before any stdio spawn path."""
|
|
3022
|
+
try:
|
|
3023
|
+
from hermes_cli.mcp_security import validate_mcp_server_entry as _validate_mcp_server_entry
|
|
3024
|
+
except Exception:
|
|
3025
|
+
_validate_mcp_server_entry: Callable[[str, dict[str, Any]], list[str]] | None = None
|
|
3026
|
+
|
|
3027
|
+
if _validate_mcp_server_entry is None:
|
|
3028
|
+
return servers
|
|
3029
|
+
|
|
3030
|
+
safe_servers = {}
|
|
3031
|
+
for name, cfg in servers.items():
|
|
3032
|
+
if not isinstance(cfg, dict):
|
|
3033
|
+
safe_servers[name] = cfg
|
|
3034
|
+
continue
|
|
3035
|
+
issues = _validate_mcp_server_entry(name, cfg)
|
|
3036
|
+
if issues:
|
|
3037
|
+
logger.warning(
|
|
3038
|
+
"Skipping suspicious MCP server '%s': %s",
|
|
3039
|
+
name,
|
|
3040
|
+
"; ".join(issues),
|
|
3041
|
+
)
|
|
3042
|
+
continue
|
|
3043
|
+
safe_servers[name] = cfg
|
|
3044
|
+
return safe_servers
|
|
3045
|
+
|
|
3046
|
+
|
|
2537
3047
|
def _load_mcp_config() -> Dict[str, dict]:
|
|
2538
3048
|
"""Read ``mcp_servers`` from the Hermes config file.
|
|
2539
3049
|
|
|
@@ -2547,6 +3057,11 @@ def _load_mcp_config() -> Dict[str, dict]:
|
|
|
2547
3057
|
"""
|
|
2548
3058
|
try:
|
|
2549
3059
|
from hermes_cli.config import load_config
|
|
3060
|
+
# Safe mode (--safe-mode / HERMES_SAFE_MODE=1): troubleshooting run
|
|
3061
|
+
# with all customizations disabled — no MCP servers connect.
|
|
3062
|
+
from utils import env_var_enabled as _env_enabled
|
|
3063
|
+
if _env_enabled("HERMES_SAFE_MODE"):
|
|
3064
|
+
return {}
|
|
2550
3065
|
config = load_config()
|
|
2551
3066
|
servers = config.get("mcp_servers")
|
|
2552
3067
|
if not servers or not isinstance(servers, dict):
|
|
@@ -2557,7 +3072,12 @@ def _load_mcp_config() -> Dict[str, dict]:
|
|
|
2557
3072
|
load_hermes_dotenv()
|
|
2558
3073
|
except Exception:
|
|
2559
3074
|
pass
|
|
2560
|
-
|
|
3075
|
+
safe_servers: Dict[str, dict] = {}
|
|
3076
|
+
for name, cfg in _filter_suspicious_mcp_servers(servers).items():
|
|
3077
|
+
interpolated = _interpolate_env_vars(cfg)
|
|
3078
|
+
if isinstance(interpolated, dict):
|
|
3079
|
+
safe_servers[name] = interpolated
|
|
3080
|
+
return safe_servers
|
|
2561
3081
|
except Exception as exc:
|
|
2562
3082
|
logger.debug("Failed to load MCP config: %s", exc)
|
|
2563
3083
|
return {}
|
|
@@ -2631,7 +3151,15 @@ def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
|
|
|
2631
3151
|
|
|
2632
3152
|
async def _call():
|
|
2633
3153
|
async with server._rpc_lock:
|
|
2634
|
-
|
|
3154
|
+
# Snapshot the agent's context so an elicitation callback
|
|
3155
|
+
# triggered during this call (fired on the MCP recv loop
|
|
3156
|
+
# task, which doesn't inherit our contextvars) can replay
|
|
3157
|
+
# it and detect the gateway platform / session for routing.
|
|
3158
|
+
server._pending_call_context = contextvars.copy_context()
|
|
3159
|
+
try:
|
|
3160
|
+
result = await server.session.call_tool(tool_name, arguments=args)
|
|
3161
|
+
finally:
|
|
3162
|
+
server._pending_call_context = None
|
|
2635
3163
|
# MCP CallToolResult has .content (list of content blocks) and .isError
|
|
2636
3164
|
if result.isError:
|
|
2637
3165
|
error_text = ""
|
|
@@ -3468,6 +3996,8 @@ async def _discover_and_register_server(name: str, config: dict) -> List[str]:
|
|
|
3468
3996
|
timeout=connect_timeout,
|
|
3469
3997
|
)
|
|
3470
3998
|
with _lock:
|
|
3999
|
+
_server_connecting.discard(name)
|
|
4000
|
+
_server_connect_errors.pop(name, None)
|
|
3471
4001
|
_servers[name] = server
|
|
3472
4002
|
|
|
3473
4003
|
registered_names = _register_server_tools(name, server, config)
|
|
@@ -3502,6 +4032,7 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
|
|
|
3502
4032
|
logger.debug("MCP SDK not available -- skipping explicit MCP registration")
|
|
3503
4033
|
return []
|
|
3504
4034
|
|
|
4035
|
+
servers = _filter_suspicious_mcp_servers(servers)
|
|
3505
4036
|
if not servers:
|
|
3506
4037
|
logger.debug("No explicit MCP servers provided")
|
|
3507
4038
|
return []
|
|
@@ -3514,6 +4045,9 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
|
|
|
3514
4045
|
for k, v in servers.items()
|
|
3515
4046
|
if k not in _servers and _parse_boolish(v.get("enabled", True), default=True)
|
|
3516
4047
|
}
|
|
4048
|
+
_server_connecting.update(new_servers)
|
|
4049
|
+
for srv_name in new_servers:
|
|
4050
|
+
_server_connect_errors.pop(srv_name, None)
|
|
3517
4051
|
# Track which servers opt-in to parallel tool calls (idempotent).
|
|
3518
4052
|
for srv_name, srv_cfg in servers.items():
|
|
3519
4053
|
if _parse_boolish(srv_cfg.get("supports_parallel_tool_calls", False), default=False):
|
|
@@ -3541,12 +4075,20 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
|
|
|
3541
4075
|
for name, result in zip(server_names, results):
|
|
3542
4076
|
if isinstance(result, BaseException):
|
|
3543
4077
|
command = new_servers.get(name, {}).get("command")
|
|
4078
|
+
message = _format_connect_error(result)
|
|
4079
|
+
with _lock:
|
|
4080
|
+
_server_connecting.discard(name)
|
|
4081
|
+
_server_connect_errors[name] = message
|
|
3544
4082
|
logger.warning(
|
|
3545
4083
|
"Failed to connect to MCP server '%s'%s: %s",
|
|
3546
4084
|
name,
|
|
3547
4085
|
f" (command={command})" if command else "",
|
|
3548
|
-
|
|
4086
|
+
message,
|
|
3549
4087
|
)
|
|
4088
|
+
else:
|
|
4089
|
+
with _lock:
|
|
4090
|
+
_server_connecting.discard(name)
|
|
4091
|
+
_server_connect_errors.pop(name, None)
|
|
3550
4092
|
|
|
3551
4093
|
# Per-server timeouts are handled inside _discover_and_register_server.
|
|
3552
4094
|
# The outer timeout is generous: 120s total for parallel discovery.
|
|
@@ -3651,8 +4193,10 @@ def is_mcp_tool_parallel_safe(tool_name: str) -> bool:
|
|
|
3651
4193
|
def get_mcp_status() -> List[dict]:
|
|
3652
4194
|
"""Return status of all configured MCP servers for banner display.
|
|
3653
4195
|
|
|
3654
|
-
Returns a list of dicts with keys: name, transport, tools, connected
|
|
3655
|
-
|
|
4196
|
+
Returns a list of dicts with keys: name, transport, tools, connected,
|
|
4197
|
+
disabled, and status. Includes connected servers, disabled servers,
|
|
4198
|
+
in-flight connection attempts, recorded failures, and servers that are
|
|
4199
|
+
configured but have not been started in this process yet.
|
|
3656
4200
|
"""
|
|
3657
4201
|
result: List[dict] = []
|
|
3658
4202
|
|
|
@@ -3663,9 +4207,12 @@ def get_mcp_status() -> List[dict]:
|
|
|
3663
4207
|
|
|
3664
4208
|
with _lock:
|
|
3665
4209
|
active_servers = dict(_servers)
|
|
4210
|
+
connecting = set(_server_connecting)
|
|
4211
|
+
connect_errors = dict(_server_connect_errors)
|
|
3666
4212
|
|
|
3667
4213
|
for name, cfg in configured.items():
|
|
3668
4214
|
transport = cfg.get("transport", "http") if "url" in cfg else "stdio"
|
|
4215
|
+
enabled = _parse_boolish(cfg.get("enabled", True), default=True)
|
|
3669
4216
|
server = active_servers.get(name)
|
|
3670
4217
|
if server and server.session is not None:
|
|
3671
4218
|
entry = {
|
|
@@ -3673,16 +4220,51 @@ def get_mcp_status() -> List[dict]:
|
|
|
3673
4220
|
"transport": transport,
|
|
3674
4221
|
"tools": len(server._registered_tool_names) if hasattr(server, "_registered_tool_names") else len(server._tools),
|
|
3675
4222
|
"connected": True,
|
|
4223
|
+
"disabled": False,
|
|
4224
|
+
"status": "connected",
|
|
3676
4225
|
}
|
|
3677
4226
|
if server._sampling:
|
|
3678
4227
|
entry["sampling"] = dict(server._sampling.metrics)
|
|
3679
4228
|
result.append(entry)
|
|
4229
|
+
elif not enabled:
|
|
4230
|
+
# A server with enabled: false is intentionally not connected — it is
|
|
4231
|
+
# disabled, not failed. Surface that distinction so consumers (banner,
|
|
4232
|
+
# TUI) can render "disabled" rather than an alarming "failed".
|
|
4233
|
+
result.append({
|
|
4234
|
+
"name": name,
|
|
4235
|
+
"transport": transport,
|
|
4236
|
+
"tools": 0,
|
|
4237
|
+
"connected": False,
|
|
4238
|
+
"disabled": True,
|
|
4239
|
+
"status": "disabled",
|
|
4240
|
+
})
|
|
4241
|
+
elif name in connecting:
|
|
4242
|
+
result.append({
|
|
4243
|
+
"name": name,
|
|
4244
|
+
"transport": transport,
|
|
4245
|
+
"tools": 0,
|
|
4246
|
+
"connected": False,
|
|
4247
|
+
"disabled": False,
|
|
4248
|
+
"status": "connecting",
|
|
4249
|
+
})
|
|
4250
|
+
elif name in connect_errors:
|
|
4251
|
+
result.append({
|
|
4252
|
+
"name": name,
|
|
4253
|
+
"transport": transport,
|
|
4254
|
+
"tools": 0,
|
|
4255
|
+
"connected": False,
|
|
4256
|
+
"disabled": False,
|
|
4257
|
+
"status": "failed",
|
|
4258
|
+
"error": connect_errors[name],
|
|
4259
|
+
})
|
|
3680
4260
|
else:
|
|
3681
4261
|
result.append({
|
|
3682
4262
|
"name": name,
|
|
3683
4263
|
"transport": transport,
|
|
3684
4264
|
"tools": 0,
|
|
3685
4265
|
"connected": False,
|
|
4266
|
+
"disabled": False,
|
|
4267
|
+
"status": "configured",
|
|
3686
4268
|
})
|
|
3687
4269
|
|
|
3688
4270
|
return result
|
|
@@ -3749,11 +4331,220 @@ def probe_mcp_server_tools() -> Dict[str, List[tuple]]:
|
|
|
3749
4331
|
except Exception as exc:
|
|
3750
4332
|
logger.debug("MCP probe failed: %s", exc)
|
|
3751
4333
|
finally:
|
|
3752
|
-
|
|
4334
|
+
_stop_mcp_loop_if_idle()
|
|
3753
4335
|
|
|
3754
4336
|
return result
|
|
3755
4337
|
|
|
3756
4338
|
|
|
4339
|
+
# Serializes in-place mutation of an agent's tool snapshot. The reload RPC,
|
|
4340
|
+
# the gateway reload, and the late-binding refresh thread all swap
|
|
4341
|
+
# ``agent.tools`` / ``agent.valid_tool_names`` after the agent was built; the
|
|
4342
|
+
# agent's run loop reads those during tool iteration, so a concurrent write
|
|
4343
|
+
# mid-read could otherwise expose a half-updated list.
|
|
4344
|
+
_agent_tools_lock = threading.Lock()
|
|
4345
|
+
|
|
4346
|
+
|
|
4347
|
+
def has_registered_mcp_tools() -> bool:
|
|
4348
|
+
"""True if any MCP server has actually registered tools into the registry.
|
|
4349
|
+
|
|
4350
|
+
Cheap — checks the global MCP-tool→server name map under ``_lock``, no
|
|
4351
|
+
registry walk. Used by the per-turn refresh hook so a session with no MCP
|
|
4352
|
+
tools (the common case, and also a connected-but-zero-tool/prompt-only
|
|
4353
|
+
server) skips the ``get_tool_definitions`` rebuild entirely. Checks
|
|
4354
|
+
registered TOOLS, not connected servers, so a server that registers no tools
|
|
4355
|
+
doesn't keep the hook firing every turn.
|
|
4356
|
+
"""
|
|
4357
|
+
with _lock:
|
|
4358
|
+
return bool(_mcp_tool_server_names)
|
|
4359
|
+
|
|
4360
|
+
|
|
4361
|
+
def refresh_agent_mcp_tools(
|
|
4362
|
+
agent,
|
|
4363
|
+
*,
|
|
4364
|
+
enabled_override=None,
|
|
4365
|
+
disabled_override=None,
|
|
4366
|
+
quiet_mode: bool = True,
|
|
4367
|
+
) -> set:
|
|
4368
|
+
"""Re-derive an already-built agent's tool snapshot from the live registry.
|
|
4369
|
+
|
|
4370
|
+
The agent snapshots ``agent.tools`` once at build time and never re-reads
|
|
4371
|
+
the registry (see ``run_agent`` / ``agent_init``). When MCP servers connect
|
|
4372
|
+
*after* that snapshot — a slow HTTP/OAuth server that misses the bounded
|
|
4373
|
+
startup wait, or a ``/reload-mcp`` — their tools are invisible until the
|
|
4374
|
+
snapshot is rebuilt. This is the single shared rebuild used by every such
|
|
4375
|
+
caller (the TUI ``reload.mcp`` RPC, the gateway reload, the late-binding
|
|
4376
|
+
refresh thread, and the per-turn between-turns refresh) so they can't drift
|
|
4377
|
+
apart again.
|
|
4378
|
+
|
|
4379
|
+
The rebuild respects the agent's own ``enabled_toolsets`` /
|
|
4380
|
+
``disabled_toolsets`` (the same filtering it was built with) and diffs by
|
|
4381
|
+
tool **name** (not count — a count compare misses an equal-size add/remove
|
|
4382
|
+
swap).
|
|
4383
|
+
|
|
4384
|
+
Crucially it is **additive-preserving**: ``get_tool_definitions`` returns
|
|
4385
|
+
only the registry-derived tools, but ``agent_init`` appends two further
|
|
4386
|
+
families directly onto ``agent.tools`` *after* that — external
|
|
4387
|
+
memory-provider tools (mem0/honcho/…) and context-engine tools
|
|
4388
|
+
(``lcm_*``). A naive ``agent.tools = get_tool_definitions(...)`` would
|
|
4389
|
+
silently DELETE those. So after rebuilding the registry set we re-run the
|
|
4390
|
+
same post-build injectors ``agent_init`` used, reconstructing the full
|
|
4391
|
+
surface. The new ``(tools, valid_tool_names)`` pair is published together
|
|
4392
|
+
under ``_agent_tools_lock`` so a concurrent reader never sees a
|
|
4393
|
+
cross-attribute half-swap.
|
|
4394
|
+
|
|
4395
|
+
Returns the set of newly-added tool names (empty when nothing changed), so
|
|
4396
|
+
callers can decide whether to notify the user / re-emit session info. The
|
|
4397
|
+
caller owns the prompt-cache contract: this helper does NOT check turn state,
|
|
4398
|
+
because each caller has a different policy (``/reload-mcp`` rebuilds after
|
|
4399
|
+
explicit user consent; the late-binding and between-turns paths only rebuild
|
|
4400
|
+
at a turn boundary, before that turn's ``tools=`` prefix is assembled).
|
|
4401
|
+
"""
|
|
4402
|
+
from model_tools import get_tool_definitions
|
|
4403
|
+
from tools.registry import registry
|
|
4404
|
+
|
|
4405
|
+
# Explicit reloads (/reload-mcp) pass freshly-resolved toolsets so a server
|
|
4406
|
+
# the user just ENABLED in config is picked up; the agent's stored selection
|
|
4407
|
+
# is then updated to match. The automatic paths (between-turns, late-binding)
|
|
4408
|
+
# pass nothing and reuse the agent's build-time selection unchanged.
|
|
4409
|
+
if enabled_override is not None or disabled_override is not None:
|
|
4410
|
+
enabled = enabled_override if enabled_override is not None else getattr(agent, "enabled_toolsets", None)
|
|
4411
|
+
disabled = disabled_override if disabled_override is not None else getattr(agent, "disabled_toolsets", None)
|
|
4412
|
+
agent.enabled_toolsets = enabled
|
|
4413
|
+
agent.disabled_toolsets = disabled
|
|
4414
|
+
else:
|
|
4415
|
+
enabled = getattr(agent, "enabled_toolsets", None)
|
|
4416
|
+
disabled = getattr(agent, "disabled_toolsets", None)
|
|
4417
|
+
|
|
4418
|
+
# Capture the registry generation this rebuild is derived from BEFORE the
|
|
4419
|
+
# (potentially slow) get_tool_definitions call. Used at publish time to
|
|
4420
|
+
# reject a stale write: if two callers race (e.g. the late-refresh daemon
|
|
4421
|
+
# and the between-turns prologue around turn 1), a slower caller that
|
|
4422
|
+
# computed an OLDER set must not clobber a newer set another caller already
|
|
4423
|
+
# published. ``registry._generation`` bumps on every (de)register.
|
|
4424
|
+
snapshot_generation = registry._generation
|
|
4425
|
+
|
|
4426
|
+
# Registry-derived tools (built-ins + MCP), filtered to the agent's toolsets.
|
|
4427
|
+
# Computed OUTSIDE the lock (get_tool_definitions can be slow); the diff and
|
|
4428
|
+
# publish below happen together in ONE critical section so two concurrent
|
|
4429
|
+
# callers can't torn-publish or compute overlapping ``added`` sets.
|
|
4430
|
+
new_defs = list(
|
|
4431
|
+
get_tool_definitions(
|
|
4432
|
+
enabled_toolsets=enabled,
|
|
4433
|
+
disabled_toolsets=disabled,
|
|
4434
|
+
quiet_mode=quiet_mode,
|
|
4435
|
+
)
|
|
4436
|
+
or []
|
|
4437
|
+
)
|
|
4438
|
+
new_names = {t["function"]["name"] for t in new_defs}
|
|
4439
|
+
|
|
4440
|
+
# Re-append the post-build injected families that get_tool_definitions does
|
|
4441
|
+
# NOT reproduce, so a refresh never strips them (memory-provider + context-
|
|
4442
|
+
# engine tools). Staged entirely on LOCALS — the live ``agent.tools`` /
|
|
4443
|
+
# ``valid_tool_names`` / ``_context_engine_tool_names`` are never touched
|
|
4444
|
+
# until the single atomic publish below, so a concurrent reader
|
|
4445
|
+
# (``build_api_kwargs``) can't see a partial rebuild or a cross-attribute
|
|
4446
|
+
# half-swap. ``staged_engine_names`` are the context-engine routing names
|
|
4447
|
+
# this rebuild actually appended (matching agent_init's dedup-aware add).
|
|
4448
|
+
staged_engine_names = _reinject_post_build_tools(agent, new_defs, new_names)
|
|
4449
|
+
|
|
4450
|
+
# Single atomic read-diff-publish so the returned ``added`` is consistent
|
|
4451
|
+
# with what was actually published, even under concurrent callers, and a
|
|
4452
|
+
# stale (older-generation) rebuild can't overwrite a newer published one.
|
|
4453
|
+
with _agent_tools_lock:
|
|
4454
|
+
# Defensive: the published generation should be an int, but tolerate an
|
|
4455
|
+
# agent that never set it (or set a non-int, e.g. a test mock) rather
|
|
4456
|
+
# than throwing TypeError on the comparison and silently failing the
|
|
4457
|
+
# whole refresh.
|
|
4458
|
+
published_gen_raw = getattr(agent, "_tool_snapshot_generation", -1)
|
|
4459
|
+
published_gen = published_gen_raw if isinstance(published_gen_raw, int) else -1
|
|
4460
|
+
if snapshot_generation < published_gen:
|
|
4461
|
+
# A newer snapshot already won; our set is stale — drop it.
|
|
4462
|
+
return set()
|
|
4463
|
+
current = {
|
|
4464
|
+
t["function"]["name"]
|
|
4465
|
+
for t in (getattr(agent, "tools", None) or [])
|
|
4466
|
+
}
|
|
4467
|
+
if new_names == current:
|
|
4468
|
+
# No change → leave the live snapshot untouched (no churn), but
|
|
4469
|
+
# record the generation so an in-flight older caller can't clobber.
|
|
4470
|
+
agent._tool_snapshot_generation = max(published_gen, snapshot_generation)
|
|
4471
|
+
return set()
|
|
4472
|
+
agent.tools = new_defs
|
|
4473
|
+
agent.valid_tool_names = new_names
|
|
4474
|
+
# Publish context-engine routing names atomically with the snapshot.
|
|
4475
|
+
engine_names = getattr(agent, "_context_engine_tool_names", None)
|
|
4476
|
+
if isinstance(engine_names, set):
|
|
4477
|
+
engine_names.clear()
|
|
4478
|
+
engine_names.update(staged_engine_names)
|
|
4479
|
+
agent._tool_snapshot_generation = max(published_gen, snapshot_generation)
|
|
4480
|
+
return new_names - current
|
|
4481
|
+
|
|
4482
|
+
|
|
4483
|
+
def _reinject_post_build_tools(agent, tools_list: list, name_set: set) -> set:
|
|
4484
|
+
"""Append memory-provider and context-engine tools onto staged locals.
|
|
4485
|
+
|
|
4486
|
+
Mirrors the post-``get_tool_definitions`` injection in ``agent_init`` so a
|
|
4487
|
+
snapshot rebuild reconstructs the FULL tool surface, not just the
|
|
4488
|
+
registry-derived subset. Operates ONLY on the caller's staged ``tools_list``
|
|
4489
|
+
/ ``name_set`` (never the live agent attributes) so the rebuild stays atomic.
|
|
4490
|
+
Idempotent (skips names already present) and fail-soft.
|
|
4491
|
+
|
|
4492
|
+
Returns the set of context-engine routing names actually appended by THIS
|
|
4493
|
+
rebuild — matching ``agent_init``'s dedup behavior (a name already provided
|
|
4494
|
+
by a registry/plugin tool is NOT claimed for context-engine routing). The
|
|
4495
|
+
caller publishes this into ``agent._context_engine_tool_names`` atomically
|
|
4496
|
+
with the snapshot.
|
|
4497
|
+
"""
|
|
4498
|
+
def _add(schema: dict) -> bool:
|
|
4499
|
+
name = schema.get("name", "")
|
|
4500
|
+
if not name or name in name_set:
|
|
4501
|
+
return False
|
|
4502
|
+
tools_list.append({"type": "function", "function": schema})
|
|
4503
|
+
name_set.add(name)
|
|
4504
|
+
return True
|
|
4505
|
+
|
|
4506
|
+
# Memory-provider tools (mem0/honcho/byterover/supermemory/…).
|
|
4507
|
+
try:
|
|
4508
|
+
memory_manager = getattr(agent, "_memory_manager", None)
|
|
4509
|
+
get_mem_schemas = getattr(memory_manager, "get_all_tool_schemas", None) if memory_manager else None
|
|
4510
|
+
if callable(get_mem_schemas):
|
|
4511
|
+
# Honor the same enablement gate inject_memory_provider_tools uses.
|
|
4512
|
+
from agent.memory_manager import memory_provider_tools_enabled
|
|
4513
|
+
if "memory" in name_set or memory_provider_tools_enabled(getattr(agent, "enabled_toolsets", None)):
|
|
4514
|
+
for schema in get_mem_schemas():
|
|
4515
|
+
if isinstance(schema, dict):
|
|
4516
|
+
_add(schema)
|
|
4517
|
+
except Exception:
|
|
4518
|
+
logger.debug("Memory-provider tool re-injection skipped", exc_info=True)
|
|
4519
|
+
|
|
4520
|
+
# Context-engine tools (lcm_grep/lcm_describe/…) — the `context_engine`
|
|
4521
|
+
# toolset is intentionally empty, so these only exist via this append.
|
|
4522
|
+
# Honor the same enabled_toolsets gate agent_init uses (#5544): without it a
|
|
4523
|
+
# restricted-toolset platform (e.g. platform_toolsets: telegram: []) would
|
|
4524
|
+
# re-leak lcm_* tools the build deliberately excluded, and pay the local-
|
|
4525
|
+
# model latency penalty.
|
|
4526
|
+
staged_engine_names: set = set()
|
|
4527
|
+
try:
|
|
4528
|
+
enabled = getattr(agent, "enabled_toolsets", None)
|
|
4529
|
+
context_engine_allowed = enabled is None or "context_engine" in enabled
|
|
4530
|
+
compressor = getattr(agent, "context_compressor", None)
|
|
4531
|
+
get_schemas = getattr(compressor, "get_tool_schemas", None) if compressor else None
|
|
4532
|
+
if context_engine_allowed and callable(get_schemas):
|
|
4533
|
+
for schema in get_schemas():
|
|
4534
|
+
if not isinstance(schema, dict):
|
|
4535
|
+
continue
|
|
4536
|
+
name = schema.get("name", "")
|
|
4537
|
+
# Only claim the routing name when WE appended the schema, so a
|
|
4538
|
+
# name already owned by a registry/plugin tool keeps its own
|
|
4539
|
+
# dispatch (matches agent_init.py's `continue`-before-claim).
|
|
4540
|
+
if _add(schema) and name:
|
|
4541
|
+
staged_engine_names.add(name)
|
|
4542
|
+
except Exception:
|
|
4543
|
+
logger.debug("Context-engine tool re-injection skipped", exc_info=True)
|
|
4544
|
+
|
|
4545
|
+
return staged_engine_names
|
|
4546
|
+
|
|
4547
|
+
|
|
3757
4548
|
def shutdown_mcp_servers():
|
|
3758
4549
|
"""Close all MCP server connections and stop the background loop.
|
|
3759
4550
|
|
|
@@ -3794,7 +4585,7 @@ def shutdown_mcp_servers():
|
|
|
3794
4585
|
if future is not None:
|
|
3795
4586
|
try:
|
|
3796
4587
|
future.result(timeout=15)
|
|
3797
|
-
except
|
|
4588
|
+
except BaseException as exc:
|
|
3798
4589
|
logger.debug("Error during MCP shutdown: %s", exc)
|
|
3799
4590
|
|
|
3800
4591
|
_stop_mcp_loop()
|
|
@@ -3887,10 +4678,25 @@ def _kill_orphaned_mcp_children(include_active: bool = False) -> None:
|
|
|
3887
4678
|
)
|
|
3888
4679
|
|
|
3889
4680
|
|
|
3890
|
-
def
|
|
4681
|
+
def _stop_mcp_loop_if_idle() -> bool:
|
|
4682
|
+
"""Stop the MCP loop only when no registered server still owns it.
|
|
4683
|
+
|
|
4684
|
+
Probe paths create temporary MCPServerTask instances that are not placed in
|
|
4685
|
+
``_servers``. They should clean up an otherwise-idle loop, but must not
|
|
4686
|
+
tear down the process-global loop when live agent tools are registered on
|
|
4687
|
+
it. Otherwise a dashboard/CLI probe can make later MCP tool calls fail
|
|
4688
|
+
with ``MCP event loop is not running``.
|
|
4689
|
+
"""
|
|
4690
|
+
return _stop_mcp_loop(only_if_idle=True)
|
|
4691
|
+
|
|
4692
|
+
|
|
4693
|
+
def _stop_mcp_loop(*, only_if_idle: bool = False) -> bool:
|
|
3891
4694
|
"""Stop the background event loop and join its thread."""
|
|
3892
4695
|
global _mcp_loop, _mcp_thread
|
|
3893
4696
|
with _lock:
|
|
4697
|
+
if only_if_idle and (_servers or _server_connecting):
|
|
4698
|
+
logger.debug("Leaving MCP event loop running; active servers are registered or connecting")
|
|
4699
|
+
return False
|
|
3894
4700
|
loop = _mcp_loop
|
|
3895
4701
|
thread = _mcp_thread
|
|
3896
4702
|
_mcp_loop = None
|
|
@@ -3907,3 +4713,4 @@ def _stop_mcp_loop():
|
|
|
3907
4713
|
# graceful shutdown are now orphaned — include active PIDs too
|
|
3908
4714
|
# since the loop is gone and no session can still be in flight.
|
|
3909
4715
|
_kill_orphaned_mcp_children(include_active=True)
|
|
4716
|
+
return True
|