@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
|
@@ -7,6 +7,7 @@ Handles: hermes gateway [run|start|stop|restart|status|install|uninstall|setup]
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
import shlex
|
|
10
11
|
import shutil
|
|
11
12
|
import signal
|
|
12
13
|
import subprocess
|
|
@@ -227,9 +228,8 @@ def _graceful_restart_via_sigusr1(pid: int, drain_timeout: float) -> bool:
|
|
|
227
228
|
|
|
228
229
|
SIGUSR1 is wired in gateway/run.py to ``request_restart(via_service=True)``
|
|
229
230
|
which drains in-flight agent runs (up to ``agent.restart_drain_timeout``
|
|
230
|
-
seconds), then exits. systemd
|
|
231
|
-
|
|
232
|
-
because its plist has ``KeepAlive.SuccessfulExit = false``.
|
|
231
|
+
seconds), then exits. Both systemd (``Restart=always``) and launchd
|
|
232
|
+
(unconditional ``<key>KeepAlive</key><true/>``) restart on any exit.
|
|
233
233
|
|
|
234
234
|
This is the drain-aware alternative to ``systemctl restart`` / ``SIGTERM``,
|
|
235
235
|
which SIGKILL in-flight agents after a short timeout.
|
|
@@ -319,23 +319,12 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li
|
|
|
319
319
|
# gateway. See #13242.
|
|
320
320
|
exclude_pids = exclude_pids | _get_ancestor_pids()
|
|
321
321
|
pids: list[int] = []
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"hermes_cli/main.py -p",
|
|
329
|
-
"hermes gateway",
|
|
330
|
-
# Windows: only match invocations that actually carry the ``gateway``
|
|
331
|
-
# subcommand or the gateway-dedicated console-script shim. Bare
|
|
332
|
-
# ``hermes.exe --profile`` / ``hermes.exe -p`` would also match
|
|
333
|
-
# ``hermes.exe --profile foo dashboard`` and other CLI subcommands,
|
|
334
|
-
# producing false-positive gateway PIDs (Copilot review).
|
|
335
|
-
"hermes.exe gateway",
|
|
336
|
-
"hermes-gateway.exe",
|
|
337
|
-
"gateway/run.py",
|
|
338
|
-
]
|
|
322
|
+
# Strict command-line matcher shared with gateway.status: requires the
|
|
323
|
+
# actual ``gateway run`` subcommand (or the dedicated entrypoints), so this
|
|
324
|
+
# scan no longer false-matches ``gateway status``/``dashboard`` siblings or
|
|
325
|
+
# unrelated processes like ``python -m tui_gateway``. Lazy import mirrors the
|
|
326
|
+
# circular-import avoidance used elsewhere in this module.
|
|
327
|
+
from gateway.status import looks_like_gateway_command_line
|
|
339
328
|
current_home = str(get_hermes_home().resolve())
|
|
340
329
|
current_home_lc = current_home.lower()
|
|
341
330
|
current_profile_arg = _profile_arg(current_home)
|
|
@@ -430,8 +419,7 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li
|
|
|
430
419
|
current_cmd = line[len("CommandLine=") :]
|
|
431
420
|
elif line.startswith("ProcessId="):
|
|
432
421
|
pid_str = line[len("ProcessId=") :]
|
|
433
|
-
|
|
434
|
-
if any(p in current_cmd_lc for p in patterns) and (
|
|
422
|
+
if looks_like_gateway_command_line(current_cmd) and (
|
|
435
423
|
all_profiles or _matches_current_profile(current_cmd)
|
|
436
424
|
):
|
|
437
425
|
try:
|
|
@@ -456,8 +444,7 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li
|
|
|
456
444
|
with open(f"/proc/{pid}/cmdline", "rb") as _f:
|
|
457
445
|
cmdline = _f.read().decode("utf-8", errors="replace")
|
|
458
446
|
cmdline = cmdline.replace("\x00", " ")
|
|
459
|
-
|
|
460
|
-
if any(p in cmdline_lc for p in patterns) and (
|
|
447
|
+
if looks_like_gateway_command_line(cmdline) and (
|
|
461
448
|
all_profiles or _matches_current_profile(cmdline)
|
|
462
449
|
):
|
|
463
450
|
_append_unique_pid(pids, pid, exclude_pids)
|
|
@@ -500,8 +487,7 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li
|
|
|
500
487
|
|
|
501
488
|
if pid is None:
|
|
502
489
|
continue
|
|
503
|
-
|
|
504
|
-
if any(pattern in command_lc for pattern in patterns) and (
|
|
490
|
+
if looks_like_gateway_command_line(command) and (
|
|
505
491
|
all_profiles or _matches_current_profile(command)
|
|
506
492
|
):
|
|
507
493
|
_append_unique_pid(pids, pid, exclude_pids)
|
|
@@ -642,7 +628,10 @@ def launch_detached_profile_gateway_restart(profile: str, old_pid: int) -> bool:
|
|
|
642
628
|
#
|
|
643
629
|
# ``windows_detach_popen_kwargs()`` returns the right kwargs for the
|
|
644
630
|
# host platform and is a no-op on POSIX (just ``start_new_session=True``).
|
|
645
|
-
from hermes_cli._subprocess_compat import
|
|
631
|
+
from hermes_cli._subprocess_compat import (
|
|
632
|
+
windows_detach_flags_without_breakaway,
|
|
633
|
+
windows_detach_popen_kwargs,
|
|
634
|
+
)
|
|
646
635
|
|
|
647
636
|
watcher = textwrap.dedent(
|
|
648
637
|
"""
|
|
@@ -665,6 +654,10 @@ def launch_detached_profile_gateway_restart(profile: str, old_pid: int) -> bool:
|
|
|
665
654
|
# Platform-appropriate detach for the respawned gateway. On POSIX
|
|
666
655
|
# start_new_session=True maps to os.setsid; on Windows we need
|
|
667
656
|
# explicit creationflags because start_new_session is a no-op there.
|
|
657
|
+
# CREATE_BREAKAWAY_FROM_JOB is critical: the watcher itself may have
|
|
658
|
+
# been spawned inside a job object (Electron/Tauri parent), and
|
|
659
|
+
# without breakaway the respawned gateway would die when that job
|
|
660
|
+
# tears down. See _subprocess_compat.windows_detach_flags().
|
|
668
661
|
_popen_kwargs = {
|
|
669
662
|
"stdout": subprocess.DEVNULL,
|
|
670
663
|
"stderr": subprocess.DEVNULL,
|
|
@@ -673,32 +666,67 @@ def launch_detached_profile_gateway_restart(profile: str, old_pid: int) -> bool:
|
|
|
673
666
|
_CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
674
667
|
_DETACHED_PROCESS = 0x00000008
|
|
675
668
|
_CREATE_NO_WINDOW = 0x08000000
|
|
676
|
-
|
|
677
|
-
|
|
669
|
+
_CREATE_BREAKAWAY_FROM_JOB = 0x01000000
|
|
670
|
+
_flags = (
|
|
671
|
+
_CREATE_NEW_PROCESS_GROUP
|
|
672
|
+
| _DETACHED_PROCESS
|
|
673
|
+
| _CREATE_NO_WINDOW
|
|
674
|
+
| _CREATE_BREAKAWAY_FROM_JOB
|
|
678
675
|
)
|
|
676
|
+
try:
|
|
677
|
+
_popen_kwargs["creationflags"] = _flags
|
|
678
|
+
subprocess.Popen(cmd, **_popen_kwargs)
|
|
679
|
+
except OSError:
|
|
680
|
+
# CREATE_BREAKAWAY_FROM_JOB can be rejected with
|
|
681
|
+
# ERROR_ACCESS_DENIED when the parent's job object refuses
|
|
682
|
+
# breakaway. Retry without it — DETACHED_PROCESS et al.
|
|
683
|
+
# alone are enough in most setups. Mirrors the canonical
|
|
684
|
+
# fallback in gateway_windows._spawn_detached.
|
|
685
|
+
_popen_kwargs["creationflags"] = _flags & ~_CREATE_BREAKAWAY_FROM_JOB
|
|
686
|
+
subprocess.Popen(cmd, **_popen_kwargs)
|
|
679
687
|
else:
|
|
680
688
|
_popen_kwargs["start_new_session"] = True
|
|
681
|
-
|
|
689
|
+
subprocess.Popen(cmd, **_popen_kwargs)
|
|
682
690
|
"""
|
|
683
691
|
).strip()
|
|
684
692
|
|
|
693
|
+
watcher_argv = [
|
|
694
|
+
sys.executable,
|
|
695
|
+
"-c",
|
|
696
|
+
watcher,
|
|
697
|
+
str(old_pid),
|
|
698
|
+
*_gateway_run_args_for_profile(profile),
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
# Same platform-aware detach for the watcher process itself — so
|
|
702
|
+
# closing the user's terminal doesn't kill the watcher.
|
|
685
703
|
try:
|
|
686
|
-
# Same platform-aware detach for the watcher process itself — so
|
|
687
|
-
# closing the user's terminal doesn't kill the watcher.
|
|
688
704
|
subprocess.Popen(
|
|
689
|
-
|
|
690
|
-
sys.executable,
|
|
691
|
-
"-c",
|
|
692
|
-
watcher,
|
|
693
|
-
str(old_pid),
|
|
694
|
-
*_gateway_run_args_for_profile(profile),
|
|
695
|
-
],
|
|
705
|
+
watcher_argv,
|
|
696
706
|
stdout=subprocess.DEVNULL,
|
|
697
707
|
stderr=subprocess.DEVNULL,
|
|
698
708
|
**windows_detach_popen_kwargs(),
|
|
699
709
|
)
|
|
700
710
|
except OSError:
|
|
701
|
-
|
|
711
|
+
# CREATE_BREAKAWAY_FROM_JOB rejected by the parent job object
|
|
712
|
+
# (Electron, Windows Terminal with restrictive job settings, …).
|
|
713
|
+
# Retry without it. POSIX never reaches this branch — there
|
|
714
|
+
# ``start_new_session=True`` cannot raise OSError — so the
|
|
715
|
+
# fallback is only meaningful on Windows.
|
|
716
|
+
try:
|
|
717
|
+
fallback_kwargs: dict = (
|
|
718
|
+
{"creationflags": windows_detach_flags_without_breakaway()}
|
|
719
|
+
if sys.platform == "win32"
|
|
720
|
+
else {"start_new_session": True}
|
|
721
|
+
)
|
|
722
|
+
subprocess.Popen(
|
|
723
|
+
watcher_argv,
|
|
724
|
+
stdout=subprocess.DEVNULL,
|
|
725
|
+
stderr=subprocess.DEVNULL,
|
|
726
|
+
**fallback_kwargs,
|
|
727
|
+
)
|
|
728
|
+
except OSError:
|
|
729
|
+
return False
|
|
702
730
|
return True
|
|
703
731
|
|
|
704
732
|
|
|
@@ -1054,11 +1082,30 @@ def get_gateway_runtime_snapshot(system: bool = False) -> GatewayRuntimeSnapshot
|
|
|
1054
1082
|
# Other container runtimes (or containers built before Phase 2)
|
|
1055
1083
|
# still get the original "docker (foreground)" label.
|
|
1056
1084
|
try:
|
|
1057
|
-
from hermes_cli.service_manager import detect_service_manager
|
|
1085
|
+
from hermes_cli.service_manager import detect_service_manager, get_service_manager
|
|
1058
1086
|
if detect_service_manager() == "s6":
|
|
1087
|
+
profile = _profile_suffix() or "default"
|
|
1088
|
+
service_name = f"gateway-{profile}"
|
|
1089
|
+
mgr = get_service_manager()
|
|
1090
|
+
service_installed = False
|
|
1091
|
+
service_running = False
|
|
1092
|
+
try:
|
|
1093
|
+
service_dir = getattr(mgr, "scandir", None)
|
|
1094
|
+
if service_dir is not None:
|
|
1095
|
+
service_installed = (service_dir / service_name).is_dir()
|
|
1096
|
+
except Exception:
|
|
1097
|
+
service_installed = False
|
|
1098
|
+
if service_installed:
|
|
1099
|
+
try:
|
|
1100
|
+
service_running = bool(mgr.is_running(service_name))
|
|
1101
|
+
except Exception:
|
|
1102
|
+
service_running = False
|
|
1059
1103
|
return GatewayRuntimeSnapshot(
|
|
1060
1104
|
manager="s6 (container supervisor)",
|
|
1105
|
+
service_installed=service_installed,
|
|
1106
|
+
service_running=service_running,
|
|
1061
1107
|
gateway_pids=gateway_pids,
|
|
1108
|
+
service_scope="s6",
|
|
1062
1109
|
)
|
|
1063
1110
|
except Exception:
|
|
1064
1111
|
pass # Fall through to the legacy label on any detection error.
|
|
@@ -1389,7 +1436,7 @@ def _profile_suffix() -> str:
|
|
|
1389
1436
|
return hashlib.sha256(str(home).encode()).hexdigest()[:8]
|
|
1390
1437
|
|
|
1391
1438
|
|
|
1392
|
-
def _profile_arg(hermes_home: str | None = None) -> str:
|
|
1439
|
+
def _profile_arg(hermes_home: str | None = None, default_root: str | Path | None = None) -> str:
|
|
1393
1440
|
"""Return ``--profile <name>`` only when HERMES_HOME is a named profile.
|
|
1394
1441
|
|
|
1395
1442
|
For ``~/.hermes/profiles/<name>``, returns ``"--profile <name>"``.
|
|
@@ -1399,12 +1446,16 @@ def _profile_arg(hermes_home: str | None = None) -> str:
|
|
|
1399
1446
|
hermes_home: Optional explicit HERMES_HOME path. Defaults to the current
|
|
1400
1447
|
``get_hermes_home()`` value. Should be passed when generating a
|
|
1401
1448
|
service definition for a different user (e.g. system service).
|
|
1449
|
+
default_root: Optional Hermes root to compare against. Used when
|
|
1450
|
+
generating a system service for another user from a sudo/root
|
|
1451
|
+
process, where ``Path.home()`` and ``get_default_hermes_root()``
|
|
1452
|
+
refer to root but the target profile lives under the service user.
|
|
1402
1453
|
"""
|
|
1403
1454
|
import re
|
|
1404
1455
|
from hermes_constants import get_default_hermes_root
|
|
1405
1456
|
|
|
1406
1457
|
home = Path(hermes_home or str(get_hermes_home())).resolve()
|
|
1407
|
-
default = get_default_hermes_root().resolve()
|
|
1458
|
+
default = Path(default_root).resolve() if default_root else get_default_hermes_root().resolve()
|
|
1408
1459
|
if home == default:
|
|
1409
1460
|
return ""
|
|
1410
1461
|
profiles_root = (default / "profiles").resolve()
|
|
@@ -1418,6 +1469,16 @@ def _profile_arg(hermes_home: str | None = None) -> str:
|
|
|
1418
1469
|
return ""
|
|
1419
1470
|
|
|
1420
1471
|
|
|
1472
|
+
def _profile_arg_for_target_user(hermes_home: str, target_home_dir: str) -> str:
|
|
1473
|
+
"""Return the profile arg for a system service running as another user."""
|
|
1474
|
+
target_root = Path(target_home_dir) / ".hermes"
|
|
1475
|
+
try:
|
|
1476
|
+
Path(hermes_home).resolve().relative_to(target_root.resolve())
|
|
1477
|
+
return _profile_arg(hermes_home, default_root=target_root)
|
|
1478
|
+
except ValueError:
|
|
1479
|
+
return _profile_arg(hermes_home)
|
|
1480
|
+
|
|
1481
|
+
|
|
1421
1482
|
def get_service_name() -> str:
|
|
1422
1483
|
"""Derive a systemd service name scoped to this HERMES_HOME.
|
|
1423
1484
|
|
|
@@ -2343,7 +2404,7 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)
|
|
|
2343
2404
|
if system:
|
|
2344
2405
|
username, group_name, home_dir = _system_service_identity(run_as_user)
|
|
2345
2406
|
hermes_home = _hermes_home_for_target_user(home_dir)
|
|
2346
|
-
profile_arg =
|
|
2407
|
+
profile_arg = _profile_arg_for_target_user(hermes_home, home_dir)
|
|
2347
2408
|
# Remap all paths that may resolve under the calling user's home
|
|
2348
2409
|
# (e.g. /root/) to the target user's home so the service can
|
|
2349
2410
|
# actually access them.
|
|
@@ -2368,7 +2429,7 @@ StartLimitIntervalSec=0
|
|
|
2368
2429
|
Type=simple
|
|
2369
2430
|
User={username}
|
|
2370
2431
|
Group={group_name}
|
|
2371
|
-
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run
|
|
2432
|
+
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run
|
|
2372
2433
|
WorkingDirectory={working_dir}
|
|
2373
2434
|
Environment="HOME={home_dir}"
|
|
2374
2435
|
Environment="USER={username}"
|
|
@@ -2378,8 +2439,6 @@ Environment="VIRTUAL_ENV={venv_dir}"
|
|
|
2378
2439
|
Environment="HERMES_HOME={hermes_home}"
|
|
2379
2440
|
Restart=always
|
|
2380
2441
|
RestartSec=5
|
|
2381
|
-
RestartMaxDelaySec=300
|
|
2382
|
-
RestartSteps=5
|
|
2383
2442
|
RestartForceExitStatus={GATEWAY_SERVICE_RESTART_EXIT_CODE}
|
|
2384
2443
|
KillMode=mixed
|
|
2385
2444
|
KillSignal=SIGTERM
|
|
@@ -2406,15 +2465,13 @@ StartLimitIntervalSec=0
|
|
|
2406
2465
|
|
|
2407
2466
|
[Service]
|
|
2408
2467
|
Type=simple
|
|
2409
|
-
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run
|
|
2468
|
+
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run
|
|
2410
2469
|
WorkingDirectory={working_dir}
|
|
2411
2470
|
Environment="PATH={sane_path}"
|
|
2412
2471
|
Environment="VIRTUAL_ENV={venv_dir}"
|
|
2413
2472
|
Environment="HERMES_HOME={hermes_home}"
|
|
2414
2473
|
Restart=always
|
|
2415
2474
|
RestartSec=5
|
|
2416
|
-
RestartMaxDelaySec=300
|
|
2417
|
-
RestartSteps=5
|
|
2418
2475
|
RestartForceExitStatus={GATEWAY_SERVICE_RESTART_EXIT_CODE}
|
|
2419
2476
|
KillMode=mixed
|
|
2420
2477
|
KillSignal=SIGTERM
|
|
@@ -2432,6 +2489,29 @@ def _normalize_service_definition(text: str) -> str:
|
|
|
2432
2489
|
return "\n".join(line.rstrip() for line in text.strip().splitlines())
|
|
2433
2490
|
|
|
2434
2491
|
|
|
2492
|
+
# Directives that older systemd versions silently ignore/strip. Normalize
|
|
2493
|
+
# them out of stale-check comparisons so a unit that differs only by these
|
|
2494
|
+
# directives is not perpetually flagged as outdated.
|
|
2495
|
+
_SYSTEMD_OPTIONAL_DIRECTIVES = (
|
|
2496
|
+
"RestartMaxDelaySec",
|
|
2497
|
+
"RestartSteps",
|
|
2498
|
+
)
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
def _strip_optional_systemd_directives(text: str) -> str:
|
|
2502
|
+
"""Remove systemd directives that older hosts silently drop."""
|
|
2503
|
+
lines = text.splitlines()
|
|
2504
|
+
filtered = []
|
|
2505
|
+
for line in lines:
|
|
2506
|
+
stripped = line.strip()
|
|
2507
|
+
if stripped and not stripped.startswith("#"):
|
|
2508
|
+
key = stripped.split("=", 1)[0].strip()
|
|
2509
|
+
if key in _SYSTEMD_OPTIONAL_DIRECTIVES:
|
|
2510
|
+
continue
|
|
2511
|
+
filtered.append(line)
|
|
2512
|
+
return "\n".join(filtered)
|
|
2513
|
+
|
|
2514
|
+
|
|
2435
2515
|
def _normalize_launchd_plist_for_comparison(text: str) -> str:
|
|
2436
2516
|
"""Normalize launchd plist text for staleness checks.
|
|
2437
2517
|
|
|
@@ -2459,9 +2539,75 @@ def systemd_unit_is_current(system: bool = False) -> bool:
|
|
|
2459
2539
|
installed = unit_path.read_text(encoding="utf-8")
|
|
2460
2540
|
expected_user = _read_systemd_user_from_unit(unit_path) if system else None
|
|
2461
2541
|
expected = generate_systemd_unit(system=system, run_as_user=expected_user)
|
|
2462
|
-
|
|
2463
|
-
|
|
2542
|
+
# Normalize out directives that older systemd versions silently drop
|
|
2543
|
+
# (RestartMaxDelaySec, RestartSteps) so a unit that differs only by
|
|
2544
|
+
# those directives is not perpetually flagged as outdated.
|
|
2545
|
+
norm_installed = _normalize_service_definition(
|
|
2546
|
+
_strip_optional_systemd_directives(installed)
|
|
2464
2547
|
)
|
|
2548
|
+
norm_expected = _normalize_service_definition(
|
|
2549
|
+
_strip_optional_systemd_directives(expected)
|
|
2550
|
+
)
|
|
2551
|
+
return norm_installed == norm_expected
|
|
2552
|
+
|
|
2553
|
+
|
|
2554
|
+
def _temp_home_in_service_definition(definition: str) -> str | None:
|
|
2555
|
+
"""Return the temp-dir HERMES_HOME baked into a service definition, or None.
|
|
2556
|
+
|
|
2557
|
+
A generated systemd unit / launchd plist carries the resolved HERMES_HOME
|
|
2558
|
+
in its environment block. If that path lives under the system temp dir,
|
|
2559
|
+
the definition was almost certainly generated by a test/E2E harness that
|
|
2560
|
+
exported a throwaway ``HERMES_HOME=/tmp/...`` — writing it to the real
|
|
2561
|
+
service file silently breaks the user's gateway on the next (re)start:
|
|
2562
|
+
the gateway comes back "active (running)" but pointed at an empty temp
|
|
2563
|
+
home ("No messaging platforms enabled"), deaf to every platform.
|
|
2564
|
+
Seen live 2026-06-11: an E2E guard probe ran ``hermes gateway restart``
|
|
2565
|
+
with ``HERMES_HOME=/tmp/hermes-e2e-<pr>`` exported; the restart path's
|
|
2566
|
+
unit refresh baked the temp path into the production unit and the
|
|
2567
|
+
post-update restart produced a zombie gateway for 7+ hours.
|
|
2568
|
+
|
|
2569
|
+
Matches both systemd ``Environment="HERMES_HOME=..."`` lines and launchd
|
|
2570
|
+
``<key>HERMES_HOME</key><string>...</string>`` pairs.
|
|
2571
|
+
"""
|
|
2572
|
+
import re
|
|
2573
|
+
import tempfile
|
|
2574
|
+
|
|
2575
|
+
candidates = re.findall(r'HERMES_HOME=([^"\n]+)', definition)
|
|
2576
|
+
candidates += re.findall(
|
|
2577
|
+
r"<key>HERMES_HOME</key>\s*<string>(.*?)</string>", definition, flags=re.S
|
|
2578
|
+
)
|
|
2579
|
+
temp_roots = {
|
|
2580
|
+
Path(tempfile.gettempdir()).resolve(),
|
|
2581
|
+
Path("/tmp"),
|
|
2582
|
+
Path("/var/tmp"),
|
|
2583
|
+
Path("/private/tmp"),
|
|
2584
|
+
Path("/private/var/tmp"),
|
|
2585
|
+
}
|
|
2586
|
+
for raw in candidates:
|
|
2587
|
+
try:
|
|
2588
|
+
resolved = Path(raw.strip().strip('"')).resolve()
|
|
2589
|
+
except (OSError, ValueError):
|
|
2590
|
+
continue
|
|
2591
|
+
for root in temp_roots:
|
|
2592
|
+
if resolved == root or root in resolved.parents:
|
|
2593
|
+
return raw.strip()
|
|
2594
|
+
return None
|
|
2595
|
+
|
|
2596
|
+
|
|
2597
|
+
def _refuse_temp_home_service_write(definition: str, kind: str) -> bool:
|
|
2598
|
+
"""Refuse (with guidance) when a service definition carries a temp HERMES_HOME."""
|
|
2599
|
+
temp_home = _temp_home_in_service_definition(definition)
|
|
2600
|
+
if temp_home is None:
|
|
2601
|
+
return False
|
|
2602
|
+
print(
|
|
2603
|
+
f"✗ Refusing to write the gateway {kind}: HERMES_HOME resolves to a "
|
|
2604
|
+
f"temporary directory ({temp_home})."
|
|
2605
|
+
)
|
|
2606
|
+
print(
|
|
2607
|
+
" This usually means a test/E2E environment exported HERMES_HOME. "
|
|
2608
|
+
"Unset it (or run from a clean shell) and retry."
|
|
2609
|
+
)
|
|
2610
|
+
return True
|
|
2465
2611
|
|
|
2466
2612
|
|
|
2467
2613
|
def refresh_systemd_unit_if_needed(system: bool = False) -> bool:
|
|
@@ -2494,6 +2640,12 @@ def refresh_systemd_unit_if_needed(system: bool = False) -> bool:
|
|
|
2494
2640
|
):
|
|
2495
2641
|
return False
|
|
2496
2642
|
|
|
2643
|
+
# Structural variant of the same belt: refuse to bake ANY temp-dir
|
|
2644
|
+
# HERMES_HOME into the unit (manual E2E homes like /tmp/hermes-e2e-NNN
|
|
2645
|
+
# don't carry the pytest markers above but poison the unit identically).
|
|
2646
|
+
if _refuse_temp_home_service_write(new_unit, "systemd unit"):
|
|
2647
|
+
return False
|
|
2648
|
+
|
|
2497
2649
|
unit_path.write_text(new_unit, encoding="utf-8")
|
|
2498
2650
|
_run_systemctl(["daemon-reload"], system=system, check=True, timeout=30)
|
|
2499
2651
|
print(
|
|
@@ -2662,10 +2814,11 @@ def systemd_install(
|
|
|
2662
2814
|
return
|
|
2663
2815
|
|
|
2664
2816
|
unit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2817
|
+
new_unit = generate_systemd_unit(system=system, run_as_user=run_as_user)
|
|
2818
|
+
if _refuse_temp_home_service_write(new_unit, "systemd unit"):
|
|
2819
|
+
return
|
|
2665
2820
|
print(f"Installing {_service_scope_label(system)} systemd service to: {unit_path}")
|
|
2666
|
-
unit_path.write_text(
|
|
2667
|
-
generate_systemd_unit(system=system, run_as_user=run_as_user), encoding="utf-8"
|
|
2668
|
-
)
|
|
2821
|
+
unit_path.write_text(new_unit, encoding="utf-8")
|
|
2669
2822
|
|
|
2670
2823
|
_run_systemctl(["daemon-reload"], system=system, check=True, timeout=30)
|
|
2671
2824
|
if enable_on_startup:
|
|
@@ -3000,8 +3153,178 @@ def get_launchd_label() -> str:
|
|
|
3000
3153
|
return f"ai.hermes.gateway-{suffix}" if suffix else "ai.hermes.gateway"
|
|
3001
3154
|
|
|
3002
3155
|
|
|
3156
|
+
# Cached launchd domain result — probing is cheap but should only run once per
|
|
3157
|
+
# process invocation (each ``hermes gateway start/stop/status`` call).
|
|
3158
|
+
_resolved_launchd_domain: str | None = None
|
|
3159
|
+
|
|
3160
|
+
|
|
3003
3161
|
def _launchd_domain() -> str:
|
|
3004
|
-
|
|
3162
|
+
"""Return the launchd domain that actually manages the gateway service.
|
|
3163
|
+
|
|
3164
|
+
Probes ``gui/<uid>`` first (Aqua sessions), then ``user/<uid>``
|
|
3165
|
+
(Background/SSH sessions). When neither domain contains a loaded
|
|
3166
|
+
service, falls back to ``launchctl managername`` as a heuristic.
|
|
3167
|
+
|
|
3168
|
+
The result is cached for the lifetime of the process so that repeated
|
|
3169
|
+
calls (``start``, ``stop``, ``restart``) use a consistent domain.
|
|
3170
|
+
|
|
3171
|
+
See #40831, #23387.
|
|
3172
|
+
"""
|
|
3173
|
+
global _resolved_launchd_domain
|
|
3174
|
+
if _resolved_launchd_domain is not None:
|
|
3175
|
+
return _resolved_launchd_domain
|
|
3176
|
+
|
|
3177
|
+
uid = os.getuid() # windows-footgun: ok — POSIX launchd (macOS) helper, never invoked on Windows
|
|
3178
|
+
label = get_launchd_label()
|
|
3179
|
+
gui_domain = f"gui/{uid}"
|
|
3180
|
+
user_domain = f"user/{uid}"
|
|
3181
|
+
|
|
3182
|
+
# 1. Probe gui/<uid> first — in Aqua sessions the service is loaded here.
|
|
3183
|
+
try:
|
|
3184
|
+
subprocess.run(
|
|
3185
|
+
["launchctl", "print", f"{gui_domain}/{label}"],
|
|
3186
|
+
check=True,
|
|
3187
|
+
timeout=5,
|
|
3188
|
+
capture_output=True,
|
|
3189
|
+
)
|
|
3190
|
+
_resolved_launchd_domain = gui_domain
|
|
3191
|
+
return gui_domain
|
|
3192
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
3193
|
+
pass
|
|
3194
|
+
|
|
3195
|
+
# 2. Probe user/<uid> — in Background/SSH sessions this is the working domain.
|
|
3196
|
+
try:
|
|
3197
|
+
subprocess.run(
|
|
3198
|
+
["launchctl", "print", f"{user_domain}/{label}"],
|
|
3199
|
+
check=True,
|
|
3200
|
+
timeout=5,
|
|
3201
|
+
capture_output=True,
|
|
3202
|
+
)
|
|
3203
|
+
_resolved_launchd_domain = user_domain
|
|
3204
|
+
return user_domain
|
|
3205
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
3206
|
+
pass
|
|
3207
|
+
|
|
3208
|
+
# 3. Neither domain has the service loaded — use managername as heuristic.
|
|
3209
|
+
# Aqua → gui/<uid>, anything else (Background, loginwindow) → user/<uid>.
|
|
3210
|
+
try:
|
|
3211
|
+
result = subprocess.run(
|
|
3212
|
+
["launchctl", "managername"],
|
|
3213
|
+
capture_output=True,
|
|
3214
|
+
text=True,
|
|
3215
|
+
timeout=5,
|
|
3216
|
+
)
|
|
3217
|
+
if "Aqua" in (result.stdout or ""):
|
|
3218
|
+
_resolved_launchd_domain = gui_domain
|
|
3219
|
+
return gui_domain
|
|
3220
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
3221
|
+
pass
|
|
3222
|
+
|
|
3223
|
+
# 4. Default to user/<uid> (matches the pre-probing behavior for
|
|
3224
|
+
# Background/SSH sessions and is the recommended domain on macOS 26+).
|
|
3225
|
+
_resolved_launchd_domain = user_domain
|
|
3226
|
+
return user_domain
|
|
3227
|
+
|
|
3228
|
+
|
|
3229
|
+
# On macOS, exit code 125 ("Domain does not support specified action") and
|
|
3230
|
+
# 3/113 ("Could not find service") all mean the job isn't currently loaded in
|
|
3231
|
+
# the target domain, so start/restart should re-bootstrap the plist and retry.
|
|
3232
|
+
_LAUNCHD_JOB_UNLOADED_EXIT_CODES = frozenset({3, 113, 125})
|
|
3233
|
+
|
|
3234
|
+
# When even a fresh bootstrap can't manage the domain, launchctl returns 5
|
|
3235
|
+
# ("Input/output error") or a persistent 125. On those hosts launchd cannot
|
|
3236
|
+
# supervise the gateway at all, so we degrade to a detached background process
|
|
3237
|
+
# (the documented `nohup hermes gateway run` workaround). See #23387.
|
|
3238
|
+
_LAUNCHCTL_DOMAIN_UNSUPPORTED_CODES = frozenset({5, 125})
|
|
3239
|
+
|
|
3240
|
+
|
|
3241
|
+
def _launchd_error_indicates_unloaded(exc: subprocess.CalledProcessError) -> bool:
|
|
3242
|
+
"""True when launchctl failed because the job isn't loaded (retry bootstrap)."""
|
|
3243
|
+
return exc.returncode in _LAUNCHD_JOB_UNLOADED_EXIT_CODES
|
|
3244
|
+
|
|
3245
|
+
|
|
3246
|
+
def _launchctl_domain_unsupported(returncode: int) -> bool:
|
|
3247
|
+
"""True when launchctl can't manage the domain even after a fresh bootstrap.
|
|
3248
|
+
|
|
3249
|
+
Codes 5 and 125 persist on macOS hosts where neither `gui/<uid>` nor
|
|
3250
|
+
`user/<uid>` supports service management; treat these as "launchd
|
|
3251
|
+
unavailable" and degrade gracefully to a detached process.
|
|
3252
|
+
"""
|
|
3253
|
+
return returncode in _LAUNCHCTL_DOMAIN_UNSUPPORTED_CODES
|
|
3254
|
+
|
|
3255
|
+
|
|
3256
|
+
def _gateway_run_command() -> list[str]:
|
|
3257
|
+
"""Build the `python -m hermes_cli.main [--profile X] gateway run --replace` argv.
|
|
3258
|
+
|
|
3259
|
+
Profile-aware: honors the active HERMES_HOME via `_profile_arg()` so the
|
|
3260
|
+
detached fallback launches into the same profile as the CLI invocation.
|
|
3261
|
+
"""
|
|
3262
|
+
cmd = [get_python_path(), "-m", "hermes_cli.main"]
|
|
3263
|
+
profile_arg = _profile_arg()
|
|
3264
|
+
if profile_arg:
|
|
3265
|
+
cmd.extend(profile_arg.split())
|
|
3266
|
+
cmd.extend(["gateway", "run", "--replace"])
|
|
3267
|
+
return cmd
|
|
3268
|
+
|
|
3269
|
+
|
|
3270
|
+
def _spawn_detached_gateway() -> bool:
|
|
3271
|
+
"""Launch the gateway as a detached background process (launchd fallback).
|
|
3272
|
+
|
|
3273
|
+
Used when launchctl can no longer bootstrap/kickstart the gateway on
|
|
3274
|
+
macOS 26+ (issue #23387). Mirrors the `nohup hermes gateway run --replace`
|
|
3275
|
+
workaround but keeps it CLI-managed: stdout/stderr go to the profile's
|
|
3276
|
+
gateway logs and the PID is tracked via the gateway.pid file that
|
|
3277
|
+
`run_gateway` writes, so stop/status/restart keep working.
|
|
3278
|
+
"""
|
|
3279
|
+
from hermes_cli._subprocess_compat import windows_detach_popen_kwargs
|
|
3280
|
+
|
|
3281
|
+
log_dir = get_hermes_home() / "logs"
|
|
3282
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
3283
|
+
out_path = log_dir / "gateway.log"
|
|
3284
|
+
err_path = log_dir / "gateway.error.log"
|
|
3285
|
+
try:
|
|
3286
|
+
out = open(out_path, "ab")
|
|
3287
|
+
err = open(err_path, "ab")
|
|
3288
|
+
except OSError:
|
|
3289
|
+
return False
|
|
3290
|
+
try:
|
|
3291
|
+
with out, err:
|
|
3292
|
+
subprocess.Popen(
|
|
3293
|
+
_gateway_run_command(),
|
|
3294
|
+
stdin=subprocess.DEVNULL,
|
|
3295
|
+
stdout=out,
|
|
3296
|
+
stderr=err,
|
|
3297
|
+
**windows_detach_popen_kwargs(),
|
|
3298
|
+
)
|
|
3299
|
+
except OSError:
|
|
3300
|
+
return False
|
|
3301
|
+
return True
|
|
3302
|
+
|
|
3303
|
+
|
|
3304
|
+
def _launchd_fallback_to_detached(reason: str, *, exit_on_failure: bool = True) -> bool:
|
|
3305
|
+
"""Start the gateway detached when launchd can't manage it, with guidance.
|
|
3306
|
+
|
|
3307
|
+
Returns True if the detached gateway was launched. When it can't be
|
|
3308
|
+
launched, prints the manual workaround and (by default) exits non-zero so
|
|
3309
|
+
the failure surfaces instead of silently doing nothing.
|
|
3310
|
+
"""
|
|
3311
|
+
from hermes_constants import display_hermes_home as _dhh
|
|
3312
|
+
|
|
3313
|
+
print(f"⚠ launchd cannot manage the gateway on this macOS version ({reason}).")
|
|
3314
|
+
if _spawn_detached_gateway():
|
|
3315
|
+
print("✓ Started gateway as a background process instead")
|
|
3316
|
+
print(" It will NOT auto-start at login or auto-restart on crash.")
|
|
3317
|
+
print(f" Logs: {_dhh()}/logs/gateway.log")
|
|
3318
|
+
print(" Stop it with: hermes gateway stop")
|
|
3319
|
+
return True
|
|
3320
|
+
print_error("Failed to start the gateway as a background process.")
|
|
3321
|
+
print(
|
|
3322
|
+
f" Try manually: nohup hermes gateway run --replace "
|
|
3323
|
+
f"> {_dhh()}/logs/gateway.log 2>&1 &"
|
|
3324
|
+
)
|
|
3325
|
+
if exit_on_failure:
|
|
3326
|
+
sys.exit(1)
|
|
3327
|
+
return False
|
|
3005
3328
|
|
|
3006
3329
|
|
|
3007
3330
|
def generate_launchd_plist() -> str:
|
|
@@ -3078,15 +3401,18 @@ def generate_launchd_plist() -> str:
|
|
|
3078
3401
|
<key>HERMES_HOME</key>
|
|
3079
3402
|
<string>{hermes_home}</string>
|
|
3080
3403
|
</dict>
|
|
3404
|
+
|
|
3405
|
+
<key>LimitLoadToSessionType</key>
|
|
3406
|
+
<array>
|
|
3407
|
+
<string>Aqua</string>
|
|
3408
|
+
<string>Background</string>
|
|
3409
|
+
</array>
|
|
3081
3410
|
|
|
3082
3411
|
<key>RunAtLoad</key>
|
|
3083
3412
|
<true/>
|
|
3084
3413
|
|
|
3085
3414
|
<key>KeepAlive</key>
|
|
3086
|
-
<
|
|
3087
|
-
<key>SuccessfulExit</key>
|
|
3088
|
-
<false/>
|
|
3089
|
-
</dict>
|
|
3415
|
+
<true/>
|
|
3090
3416
|
|
|
3091
3417
|
<key>StandardOutPath</key>
|
|
3092
3418
|
<string>{log_dir}/gateway.log</string>
|
|
@@ -3122,16 +3448,66 @@ def refresh_launchd_plist_if_needed() -> bool:
|
|
|
3122
3448
|
if not plist_path.exists() or launchd_plist_is_current():
|
|
3123
3449
|
return False
|
|
3124
3450
|
|
|
3125
|
-
|
|
3451
|
+
new_plist = generate_launchd_plist()
|
|
3452
|
+
if _refuse_temp_home_service_write(new_plist, "launchd plist"):
|
|
3453
|
+
return False
|
|
3454
|
+
|
|
3455
|
+
plist_path.write_text(new_plist, encoding="utf-8")
|
|
3126
3456
|
label = get_launchd_label()
|
|
3457
|
+
domain = _launchd_domain()
|
|
3458
|
+
target = f"{domain}/{label}"
|
|
3459
|
+
|
|
3460
|
+
# If this refresh is running INSIDE the gateway's own launchd process tree
|
|
3461
|
+
# (e.g. the agent triggered a self-update via its terminal tool), a direct
|
|
3462
|
+
# `launchctl bootout` tears down the service's process group — which
|
|
3463
|
+
# includes THIS CLI — before the follow-up `bootstrap` can run. The gateway
|
|
3464
|
+
# then stays unloaded and KeepAlive can't revive it (#43842). Detect that
|
|
3465
|
+
# case and hand the reload to a detached session that survives the bootout.
|
|
3466
|
+
gateway_pid = None
|
|
3467
|
+
try:
|
|
3468
|
+
from gateway.status import get_running_pid
|
|
3469
|
+
gateway_pid = get_running_pid()
|
|
3470
|
+
except Exception:
|
|
3471
|
+
gateway_pid = None
|
|
3472
|
+
|
|
3473
|
+
if (
|
|
3474
|
+
gateway_pid is not None
|
|
3475
|
+
and _is_pid_ancestor_of_current_process(gateway_pid)
|
|
3476
|
+
and hasattr(os, "setsid") # POSIX-only; launchd is macOS so always true here
|
|
3477
|
+
):
|
|
3478
|
+
# Delegate to a new session: `start_new_session=True` detaches the
|
|
3479
|
+
# helper from the gateway's process group, so the bootout that kills
|
|
3480
|
+
# the gateway (and us) does not kill the helper before it bootstraps.
|
|
3481
|
+
reload_script = (
|
|
3482
|
+
f"sleep 2; "
|
|
3483
|
+
f"launchctl bootout {shlex.quote(target)} 2>/dev/null; "
|
|
3484
|
+
f"sleep 1; "
|
|
3485
|
+
f"launchctl bootstrap {shlex.quote(domain)} {shlex.quote(str(plist_path))} 2>/dev/null"
|
|
3486
|
+
)
|
|
3487
|
+
try:
|
|
3488
|
+
subprocess.Popen(
|
|
3489
|
+
["/bin/bash", "-c", reload_script],
|
|
3490
|
+
start_new_session=True,
|
|
3491
|
+
stdout=subprocess.DEVNULL,
|
|
3492
|
+
stderr=subprocess.DEVNULL,
|
|
3493
|
+
)
|
|
3494
|
+
except Exception as e:
|
|
3495
|
+
logger.warning("Deferred launchd reload could not be spawned: %s", e)
|
|
3496
|
+
return False
|
|
3497
|
+
print(
|
|
3498
|
+
"↻ Updated gateway launchd service definition; reload deferred to a "
|
|
3499
|
+
"detached helper (refresh ran inside the gateway process tree)"
|
|
3500
|
+
)
|
|
3501
|
+
return True
|
|
3502
|
+
|
|
3127
3503
|
# Bootout/bootstrap so launchd picks up the new definition
|
|
3128
3504
|
subprocess.run(
|
|
3129
|
-
["launchctl", "bootout",
|
|
3505
|
+
["launchctl", "bootout", target],
|
|
3130
3506
|
check=False,
|
|
3131
3507
|
timeout=90,
|
|
3132
3508
|
)
|
|
3133
3509
|
subprocess.run(
|
|
3134
|
-
["launchctl", "bootstrap",
|
|
3510
|
+
["launchctl", "bootstrap", domain, str(plist_path)],
|
|
3135
3511
|
check=False,
|
|
3136
3512
|
timeout=30,
|
|
3137
3513
|
)
|
|
@@ -3155,14 +3531,23 @@ def launchd_install(force: bool = False):
|
|
|
3155
3531
|
return
|
|
3156
3532
|
|
|
3157
3533
|
plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3534
|
+
new_plist = generate_launchd_plist()
|
|
3535
|
+
if _refuse_temp_home_service_write(new_plist, "launchd plist"):
|
|
3536
|
+
return
|
|
3158
3537
|
print(f"Installing launchd service to: {plist_path}")
|
|
3159
|
-
plist_path.write_text(
|
|
3538
|
+
plist_path.write_text(new_plist)
|
|
3160
3539
|
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3540
|
+
try:
|
|
3541
|
+
subprocess.run(
|
|
3542
|
+
["launchctl", "bootstrap", _launchd_domain(), str(plist_path)],
|
|
3543
|
+
check=True,
|
|
3544
|
+
timeout=30,
|
|
3545
|
+
)
|
|
3546
|
+
except subprocess.CalledProcessError as e:
|
|
3547
|
+
if not _launchctl_domain_unsupported(e.returncode):
|
|
3548
|
+
raise
|
|
3549
|
+
_launchd_fallback_to_detached(f"launchctl bootstrap exit {e.returncode}")
|
|
3550
|
+
return
|
|
3166
3551
|
|
|
3167
3552
|
print()
|
|
3168
3553
|
print("✓ Service installed and loaded!")
|
|
@@ -3196,19 +3581,28 @@ def launchd_start():
|
|
|
3196
3581
|
|
|
3197
3582
|
# Self-heal if the plist is missing entirely (e.g., manual cleanup, failed upgrade)
|
|
3198
3583
|
if not plist_path.exists():
|
|
3584
|
+
new_plist = generate_launchd_plist()
|
|
3585
|
+
if _refuse_temp_home_service_write(new_plist, "launchd plist"):
|
|
3586
|
+
sys.exit(1)
|
|
3199
3587
|
print("↻ launchd plist missing; regenerating service definition")
|
|
3200
3588
|
plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3201
|
-
plist_path.write_text(
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3589
|
+
plist_path.write_text(new_plist, encoding="utf-8")
|
|
3590
|
+
try:
|
|
3591
|
+
subprocess.run(
|
|
3592
|
+
["launchctl", "bootstrap", _launchd_domain(), str(plist_path)],
|
|
3593
|
+
check=True,
|
|
3594
|
+
timeout=30,
|
|
3595
|
+
)
|
|
3596
|
+
subprocess.run(
|
|
3597
|
+
["launchctl", "kickstart", f"{_launchd_domain()}/{label}"],
|
|
3598
|
+
check=True,
|
|
3599
|
+
timeout=30,
|
|
3600
|
+
)
|
|
3601
|
+
except subprocess.CalledProcessError as e:
|
|
3602
|
+
if not _launchctl_domain_unsupported(e.returncode):
|
|
3603
|
+
raise
|
|
3604
|
+
_launchd_fallback_to_detached(f"launchctl exit {e.returncode}")
|
|
3605
|
+
return
|
|
3212
3606
|
print("✓ Service started")
|
|
3213
3607
|
return
|
|
3214
3608
|
|
|
@@ -3220,19 +3614,28 @@ def launchd_start():
|
|
|
3220
3614
|
timeout=30,
|
|
3221
3615
|
)
|
|
3222
3616
|
except subprocess.CalledProcessError as e:
|
|
3223
|
-
if
|
|
3617
|
+
if not _launchd_error_indicates_unloaded(e):
|
|
3224
3618
|
raise
|
|
3619
|
+
# Job not loaded in this domain — re-bootstrap the plist and retry.
|
|
3225
3620
|
print("↻ launchd job was unloaded; reloading service definition")
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3621
|
+
try:
|
|
3622
|
+
subprocess.run(
|
|
3623
|
+
["launchctl", "bootstrap", _launchd_domain(), str(plist_path)],
|
|
3624
|
+
check=True,
|
|
3625
|
+
timeout=30,
|
|
3626
|
+
)
|
|
3627
|
+
subprocess.run(
|
|
3628
|
+
["launchctl", "kickstart", f"{_launchd_domain()}/{label}"],
|
|
3629
|
+
check=True,
|
|
3630
|
+
timeout=30,
|
|
3631
|
+
)
|
|
3632
|
+
except subprocess.CalledProcessError as e2:
|
|
3633
|
+
# Even a fresh bootstrap can't manage the domain on this host —
|
|
3634
|
+
# degrade to a detached background process (issue #23387).
|
|
3635
|
+
if not _launchctl_domain_unsupported(e2.returncode):
|
|
3636
|
+
raise
|
|
3637
|
+
_launchd_fallback_to_detached(f"launchctl exit {e2.returncode}")
|
|
3638
|
+
return
|
|
3236
3639
|
print("✓ Service started")
|
|
3237
3640
|
|
|
3238
3641
|
|
|
@@ -3249,13 +3652,18 @@ def launchd_stop():
|
|
|
3249
3652
|
pass
|
|
3250
3653
|
# bootout unloads the service definition so KeepAlive doesn't respawn
|
|
3251
3654
|
# the process. A plain `kill SIGTERM` only signals the process — launchd
|
|
3252
|
-
# immediately restarts it because KeepAlive
|
|
3655
|
+
# immediately restarts it because KeepAlive is unconditionally true.
|
|
3253
3656
|
# `hermes gateway start` re-bootstraps when it detects the job is unloaded.
|
|
3254
3657
|
try:
|
|
3255
3658
|
subprocess.run(["launchctl", "bootout", target], check=True, timeout=90)
|
|
3256
3659
|
except subprocess.CalledProcessError as e:
|
|
3257
|
-
|
|
3258
|
-
|
|
3660
|
+
# Job already unloaded (3/113/125), or the domain can't be managed at
|
|
3661
|
+
# all (5/125, macOS 26+ detached-fallback process, issue #23387) — in
|
|
3662
|
+
# both cases just fall through to the PID-based kill below.
|
|
3663
|
+
if _launchd_error_indicates_unloaded(e) or _launchctl_domain_unsupported(
|
|
3664
|
+
e.returncode
|
|
3665
|
+
):
|
|
3666
|
+
pass
|
|
3259
3667
|
else:
|
|
3260
3668
|
raise
|
|
3261
3669
|
_wait_for_gateway_exit(timeout=10.0, force_after=5.0)
|
|
@@ -3339,17 +3747,29 @@ def launchd_restart():
|
|
|
3339
3747
|
subprocess.run(["launchctl", "kickstart", "-k", target], check=True, timeout=90)
|
|
3340
3748
|
print("✓ Service restarted")
|
|
3341
3749
|
except subprocess.CalledProcessError as e:
|
|
3342
|
-
if
|
|
3750
|
+
if not _launchd_error_indicates_unloaded(e):
|
|
3751
|
+
# Not a "job unloaded" code. If the domain is fundamentally
|
|
3752
|
+
# unmanageable (error 5), degrade to detached; the old process was
|
|
3753
|
+
# already drained/terminated above. Otherwise re-raise.
|
|
3754
|
+
if _launchctl_domain_unsupported(e.returncode):
|
|
3755
|
+
_launchd_fallback_to_detached(f"launchctl kickstart exit {e.returncode}")
|
|
3756
|
+
return
|
|
3343
3757
|
raise
|
|
3344
3758
|
# Job not loaded — bootstrap and start fresh
|
|
3345
3759
|
print("↻ launchd job was unloaded; reloading")
|
|
3346
3760
|
plist_path = get_launchd_plist_path()
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3761
|
+
try:
|
|
3762
|
+
subprocess.run(
|
|
3763
|
+
["launchctl", "bootstrap", _launchd_domain(), str(plist_path)],
|
|
3764
|
+
check=True,
|
|
3765
|
+
timeout=30,
|
|
3766
|
+
)
|
|
3767
|
+
subprocess.run(["launchctl", "kickstart", target], check=True, timeout=30)
|
|
3768
|
+
except subprocess.CalledProcessError as e2:
|
|
3769
|
+
if not _launchctl_domain_unsupported(e2.returncode):
|
|
3770
|
+
raise
|
|
3771
|
+
_launchd_fallback_to_detached(f"launchctl exit {e2.returncode}")
|
|
3772
|
+
return
|
|
3353
3773
|
print("✓ Service restarted")
|
|
3354
3774
|
|
|
3355
3775
|
|
|
@@ -3408,6 +3828,181 @@ def _is_official_docker_checkout() -> bool:
|
|
|
3408
3828
|
)
|
|
3409
3829
|
|
|
3410
3830
|
|
|
3831
|
+
def _running_under_gateway_supervisor() -> bool:
|
|
3832
|
+
"""Return True when this process IS the gateway a service manager launched.
|
|
3833
|
+
|
|
3834
|
+
The conflict guard below must never fire on the service's own startup, or
|
|
3835
|
+
it would wedge the unit into a respawn/refuse loop. Each supervisor exports
|
|
3836
|
+
a reliable marker into the child's environment:
|
|
3837
|
+
|
|
3838
|
+
- systemd sets ``INVOCATION_ID`` for every unit it launches (the same
|
|
3839
|
+
marker ``gateway/run.py`` already uses to pick the restart path).
|
|
3840
|
+
- launchd sets ``XPC_SERVICE_NAME`` to the job label for jobs it spawns;
|
|
3841
|
+
interactive shells inherit the sentinel ``"0"`` instead.
|
|
3842
|
+
- the s6-overlay container longrun exports ``HERMES_S6_SUPERVISED_CHILD``.
|
|
3843
|
+
"""
|
|
3844
|
+
if os.environ.get("INVOCATION_ID"):
|
|
3845
|
+
return True
|
|
3846
|
+
if os.environ.get("HERMES_S6_SUPERVISED_CHILD"):
|
|
3847
|
+
return True
|
|
3848
|
+
xpc_service = os.environ.get("XPC_SERVICE_NAME", "")
|
|
3849
|
+
if xpc_service and xpc_service != "0":
|
|
3850
|
+
return True
|
|
3851
|
+
return False
|
|
3852
|
+
|
|
3853
|
+
|
|
3854
|
+
def _guard_named_profile_under_multiplexer(force: bool = False) -> None:
|
|
3855
|
+
"""Refuse a named-profile gateway when a multiplexer is already serving it.
|
|
3856
|
+
|
|
3857
|
+
When the default profile's gateway runs with gateway.multiplex_profiles=on,
|
|
3858
|
+
it is the sole inbound process for EVERY profile on the host. Starting a
|
|
3859
|
+
separate gateway for a named profile would double-bind that profile's
|
|
3860
|
+
platforms (two pollers on one bot token, port fights). In that mode a
|
|
3861
|
+
named-profile ``hermes gateway run`` is always a misconfiguration, so we
|
|
3862
|
+
hard-error with a pointer to the multiplexer. ``--force`` overrides.
|
|
3863
|
+
|
|
3864
|
+
Inert unless ALL of: (a) this invocation is a named profile, (b) a default-
|
|
3865
|
+
profile gateway is running, (c) that gateway's config has multiplexing on.
|
|
3866
|
+
"""
|
|
3867
|
+
if force:
|
|
3868
|
+
return
|
|
3869
|
+
# (a) Are we a named profile? Default/custom-hash homes return "".
|
|
3870
|
+
try:
|
|
3871
|
+
suffix = _profile_suffix()
|
|
3872
|
+
except Exception:
|
|
3873
|
+
return
|
|
3874
|
+
if not suffix:
|
|
3875
|
+
return # default profile (or unrecognized) — this guard doesn't apply
|
|
3876
|
+
|
|
3877
|
+
try:
|
|
3878
|
+
from hermes_constants import get_default_hermes_root
|
|
3879
|
+
default_root = get_default_hermes_root()
|
|
3880
|
+
# (b) Is the default-profile gateway running?
|
|
3881
|
+
from gateway.status import get_running_pid as _default_running_pid # noqa
|
|
3882
|
+
except Exception:
|
|
3883
|
+
return
|
|
3884
|
+
|
|
3885
|
+
try:
|
|
3886
|
+
import yaml as _yaml
|
|
3887
|
+
from gateway.status import _read_pid_record # type: ignore
|
|
3888
|
+
|
|
3889
|
+
# (b) default gateway PID file present + alive
|
|
3890
|
+
default_pid_path = default_root / "gateway.pid"
|
|
3891
|
+
rec = _read_pid_record(default_pid_path)
|
|
3892
|
+
if not rec:
|
|
3893
|
+
return
|
|
3894
|
+
from gateway.status import _pid_exists, _pid_from_record
|
|
3895
|
+
pid = _pid_from_record(rec)
|
|
3896
|
+
if not pid or not _pid_exists(pid):
|
|
3897
|
+
return
|
|
3898
|
+
|
|
3899
|
+
# (c) default config has multiplexing on
|
|
3900
|
+
cfg_path = default_root / "config.yaml"
|
|
3901
|
+
if not cfg_path.exists():
|
|
3902
|
+
return
|
|
3903
|
+
with open(cfg_path, encoding="utf-8") as f:
|
|
3904
|
+
cfg = _yaml.safe_load(f) or {}
|
|
3905
|
+
multiplex = bool(
|
|
3906
|
+
cfg.get("multiplex_profiles")
|
|
3907
|
+
or (cfg.get("gateway", {}) or {}).get("multiplex_profiles")
|
|
3908
|
+
)
|
|
3909
|
+
if not multiplex:
|
|
3910
|
+
return
|
|
3911
|
+
except Exception:
|
|
3912
|
+
logger.debug("Multiplexer-conflict probe failed", exc_info=True)
|
|
3913
|
+
return
|
|
3914
|
+
|
|
3915
|
+
print_error(
|
|
3916
|
+
f"The default gateway is running as a profile multiplexer and already "
|
|
3917
|
+
f"serves profile '{suffix}'."
|
|
3918
|
+
)
|
|
3919
|
+
print(
|
|
3920
|
+
" When gateway.multiplex_profiles is on, the default gateway is the\n"
|
|
3921
|
+
" single inbound process for every profile. Starting a separate\n"
|
|
3922
|
+
" gateway for this profile would double-bind its platforms (two\n"
|
|
3923
|
+
" pollers on one bot token, port conflicts).\n"
|
|
3924
|
+
)
|
|
3925
|
+
print(" Manage the multiplexer instead (from the default profile):")
|
|
3926
|
+
print()
|
|
3927
|
+
print(" hermes gateway restart")
|
|
3928
|
+
print()
|
|
3929
|
+
print(" Pass --force to start a separate profile gateway anyway (not")
|
|
3930
|
+
print(" recommended while the multiplexer is running).")
|
|
3931
|
+
sys.exit(1)
|
|
3932
|
+
|
|
3933
|
+
|
|
3934
|
+
def _guard_supervised_gateway_conflict(force: bool = False) -> None:
|
|
3935
|
+
"""Refuse a foreground gateway when a service manager already supervises one.
|
|
3936
|
+
|
|
3937
|
+
Running ``hermes gateway run [--replace]`` (or the manual-restart fallback)
|
|
3938
|
+
from a shell on a systemd/launchd host spawns a second, long-lived
|
|
3939
|
+
dispatcher that escapes the service cgroup, survives
|
|
3940
|
+
``systemctl restart``, and becomes a silent concurrent writer on the shared
|
|
3941
|
+
kanban DB — the documented root cause of multi-writer SQLite WAL corruption
|
|
3942
|
+
(issue #35240). Pass ``--force`` to start anyway.
|
|
3943
|
+
"""
|
|
3944
|
+
if force or _running_under_gateway_supervisor():
|
|
3945
|
+
return
|
|
3946
|
+
try:
|
|
3947
|
+
snapshot = get_gateway_runtime_snapshot()
|
|
3948
|
+
except Exception:
|
|
3949
|
+
# Best-effort guard: a probe failure must never block a real startup.
|
|
3950
|
+
logger.debug("Supervised-gateway conflict probe failed", exc_info=True)
|
|
3951
|
+
return
|
|
3952
|
+
if not (snapshot.service_installed and snapshot.service_running):
|
|
3953
|
+
return
|
|
3954
|
+
|
|
3955
|
+
print_error(
|
|
3956
|
+
f"A gateway is already running under {snapshot.manager} for this profile."
|
|
3957
|
+
)
|
|
3958
|
+
print(
|
|
3959
|
+
" Starting another one from a shell leaves an orphan dispatcher that\n"
|
|
3960
|
+
" escapes the service, survives restarts, and writes to the same kanban\n"
|
|
3961
|
+
" DB concurrently — which can corrupt it. Restart the supervised gateway\n"
|
|
3962
|
+
" instead:"
|
|
3963
|
+
)
|
|
3964
|
+
print()
|
|
3965
|
+
print(" hermes gateway restart")
|
|
3966
|
+
print()
|
|
3967
|
+
print(
|
|
3968
|
+
" Pass --force to start a foreground gateway anyway (not recommended\n"
|
|
3969
|
+
" while the service is running)."
|
|
3970
|
+
)
|
|
3971
|
+
sys.exit(1)
|
|
3972
|
+
|
|
3973
|
+
|
|
3974
|
+
def _guard_existing_gateway_process_conflict(replace: bool = False) -> None:
|
|
3975
|
+
"""Refuse duplicate foreground startup before importing gateway.run.
|
|
3976
|
+
|
|
3977
|
+
``gateway.run`` performs the authoritative PID/lock check, but importing it
|
|
3978
|
+
is expensive: it pulls in model_tools/plugin discovery first. On small
|
|
3979
|
+
instances, a supervisor or dashboard loop repeatedly running bare
|
|
3980
|
+
``hermes gateway run`` can burn memory/CPU just to fail with "already
|
|
3981
|
+
running" after plugin discovery. This cheap PID-file preflight preserves the
|
|
3982
|
+
same user-facing contract while avoiding that startup work without scanning
|
|
3983
|
+
unrelated gateway processes from other HERMES_HOME roots.
|
|
3984
|
+
"""
|
|
3985
|
+
if replace or _running_under_gateway_supervisor():
|
|
3986
|
+
return
|
|
3987
|
+
try:
|
|
3988
|
+
from gateway.status import get_running_pid
|
|
3989
|
+
|
|
3990
|
+
pid = get_running_pid()
|
|
3991
|
+
except Exception:
|
|
3992
|
+
logger.debug("Existing-gateway process probe failed", exc_info=True)
|
|
3993
|
+
return
|
|
3994
|
+
if pid is None:
|
|
3995
|
+
return
|
|
3996
|
+
|
|
3997
|
+
print_error(
|
|
3998
|
+
f"Another gateway instance is already running (PID {pid})."
|
|
3999
|
+
)
|
|
4000
|
+
print(" Use 'hermes gateway restart' to replace it,")
|
|
4001
|
+
print(" or 'hermes gateway stop' first.")
|
|
4002
|
+
print(" Or use 'hermes gateway run --replace' to auto-replace.")
|
|
4003
|
+
sys.exit(1)
|
|
4004
|
+
|
|
4005
|
+
|
|
3411
4006
|
def _guard_official_docker_root_gateway() -> None:
|
|
3412
4007
|
"""Refuse gateway startup when the official Docker privilege drop was bypassed."""
|
|
3413
4008
|
if not hasattr(os, "geteuid") or os.geteuid() != 0:
|
|
@@ -3435,7 +4030,7 @@ def _guard_official_docker_root_gateway() -> None:
|
|
|
3435
4030
|
sys.exit(1)
|
|
3436
4031
|
|
|
3437
4032
|
|
|
3438
|
-
def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False):
|
|
4033
|
+
def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False, force: bool = False):
|
|
3439
4034
|
"""Run the gateway in foreground.
|
|
3440
4035
|
|
|
3441
4036
|
Args:
|
|
@@ -3444,8 +4039,13 @@ def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False):
|
|
|
3444
4039
|
replace: If True, kill any existing gateway instance before starting.
|
|
3445
4040
|
This prevents systemd restart loops when the old process
|
|
3446
4041
|
hasn't fully exited yet.
|
|
4042
|
+
force: Skip the supervised-gateway conflict guard and start even when a
|
|
4043
|
+
systemd/launchd service is already supervising this profile.
|
|
3447
4044
|
"""
|
|
3448
4045
|
_guard_official_docker_root_gateway()
|
|
4046
|
+
_guard_named_profile_under_multiplexer(force=force)
|
|
4047
|
+
_guard_supervised_gateway_conflict(force=force)
|
|
4048
|
+
_guard_existing_gateway_process_conflict(replace=replace)
|
|
3449
4049
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
3450
4050
|
|
|
3451
4051
|
# Detached Windows gateway runs must ignore console-control broadcasts
|
|
@@ -4386,6 +4986,35 @@ def _setup_standard_platform(platform: dict):
|
|
|
4386
4986
|
if not prompt_yes_no(f" Reconfigure {label}?", False):
|
|
4387
4987
|
return
|
|
4388
4988
|
|
|
4989
|
+
auto_token_saved = False
|
|
4990
|
+
auto_owner_user_id = None
|
|
4991
|
+
if platform.get("key") == "telegram":
|
|
4992
|
+
print()
|
|
4993
|
+
print_info(" Telegram can be configured automatically with a managed bot:")
|
|
4994
|
+
print_info(" [1] Automatic (scan QR → confirm in Telegram → done)")
|
|
4995
|
+
print_info(" [2] Manual BotFather token")
|
|
4996
|
+
choice = prompt(" Choice [1/2]", default="1")
|
|
4997
|
+
if choice.strip() == "1":
|
|
4998
|
+
try:
|
|
4999
|
+
from hermes_cli.telegram_managed_bot import (
|
|
5000
|
+
auto_setup_telegram_bot_result,
|
|
5001
|
+
is_valid_telegram_bot_token,
|
|
5002
|
+
)
|
|
5003
|
+
except ImportError:
|
|
5004
|
+
print_warning(" Automatic setup is unavailable in this install.")
|
|
5005
|
+
else:
|
|
5006
|
+
result = auto_setup_telegram_bot_result()
|
|
5007
|
+
if result and is_valid_telegram_bot_token(result.token):
|
|
5008
|
+
save_env_value(token_var, result.token)
|
|
5009
|
+
print_success(" Saved TELEGRAM_BOT_TOKEN")
|
|
5010
|
+
auto_token_saved = True
|
|
5011
|
+
auto_owner_user_id = result.owner_user_id
|
|
5012
|
+
else:
|
|
5013
|
+
if result:
|
|
5014
|
+
print_warning(" Automatic setup returned an invalid Telegram token.")
|
|
5015
|
+
print()
|
|
5016
|
+
print_info(" Falling back to manual setup...")
|
|
5017
|
+
|
|
4389
5018
|
allowed_val_set = None # Track if user set an allowlist (for home channel offer)
|
|
4390
5019
|
|
|
4391
5020
|
for var in platform["vars"]:
|
|
@@ -4395,8 +5024,30 @@ def _setup_standard_platform(platform: dict):
|
|
|
4395
5024
|
if existing and var["name"] != token_var:
|
|
4396
5025
|
print_info(f" Current: {existing}")
|
|
4397
5026
|
|
|
5027
|
+
if auto_token_saved and var["name"] == token_var:
|
|
5028
|
+
print_info(" Token saved by automatic setup.")
|
|
5029
|
+
continue
|
|
5030
|
+
|
|
4398
5031
|
# Allowlist fields get special handling for the deny-by-default security model
|
|
4399
5032
|
if var.get("is_allowlist"):
|
|
5033
|
+
if "TELEGRAM" in var["name"] and auto_owner_user_id:
|
|
5034
|
+
detected_id = str(auto_owner_user_id)
|
|
5035
|
+
print_success(f" Detected your Telegram user ID: {detected_id}")
|
|
5036
|
+
if prompt_yes_no(" Allow this Telegram account to use the bot?", True):
|
|
5037
|
+
extra = prompt(
|
|
5038
|
+
" Additional allowed user IDs (comma-separated, optional)",
|
|
5039
|
+
password=False,
|
|
5040
|
+
)
|
|
5041
|
+
ids = [detected_id]
|
|
5042
|
+
for uid in extra.replace(" ", "").split(","):
|
|
5043
|
+
if uid and uid not in ids:
|
|
5044
|
+
ids.append(uid)
|
|
5045
|
+
cleaned = ",".join(ids)
|
|
5046
|
+
save_env_value(var["name"], cleaned)
|
|
5047
|
+
print_success(" Saved — only these users can interact with the bot.")
|
|
5048
|
+
allowed_val_set = cleaned
|
|
5049
|
+
continue
|
|
5050
|
+
|
|
4400
5051
|
print_info(" The gateway DENIES all users by default for security.")
|
|
4401
5052
|
print_info(" Enter user IDs to create an allowlist, or leave empty")
|
|
4402
5053
|
print_info(" and you'll be asked about open access next.")
|
|
@@ -5940,7 +6591,8 @@ def _gateway_command_inner(args):
|
|
|
5940
6591
|
verbose = getattr(args, "verbose", 0)
|
|
5941
6592
|
quiet = getattr(args, "quiet", False)
|
|
5942
6593
|
replace = getattr(args, "replace", False)
|
|
5943
|
-
|
|
6594
|
+
force = getattr(args, "force", False)
|
|
6595
|
+
run_gateway(verbose, quiet=quiet, replace=replace, force=force)
|
|
5944
6596
|
return
|
|
5945
6597
|
|
|
5946
6598
|
if subcmd == "setup":
|