mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-09 03:11:58 +00:00
Native Windows (with Git for Windows installed) can now run the Hermes CLI and gateway end-to-end without crashing. install.ps1 already existed and the Git Bash terminal backend was already wired up — this PR fills the remaining gaps discovered by auditing every Windows-unsafe primitive (`signal.SIGKILL`, `os.kill(pid, 0)` probes, bare `fcntl`/`termios` imports) and by comparing hermes against how Claude Code, OpenCode, Codex, and Cline handle native Windows. ## What changed ### UTF-8 stdio (new module) - `hermes_cli/stdio.py` — single `configure_windows_stdio()` entry point. Flips the console code page to CP_UTF8 (65001), reconfigures `sys.stdout`/`stderr`/`stdin` to UTF-8, sets `PYTHONIOENCODING` + `PYTHONUTF8` for subprocesses. No-op on non-Windows. Opt out via `HERMES_DISABLE_WINDOWS_UTF8=1`. - Called early in `cli.py::main`, `hermes_cli/main.py::main`, and `gateway/run.py::main` so Unicode banners (box-drawing, geometric symbols, non-Latin chat text) don't `UnicodeEncodeError` on cp1252 consoles. ### Crash sites fixed - `hermes_cli/main.py:7970` (hermes update → stuck gateway sweep): raw `os.kill(pid, _signal.SIGKILL)` → `gateway.status.terminate_pid(pid, force=True)` which routes through `taskkill /T /F` on Windows. - `hermes_cli/profiles.py::_stop_gateway_process`: same fix — also converted SIGTERM path to `terminate_pid()` and widened OSError catch on the intermediate `os.kill(pid, 0)` probe. - `hermes_cli/kanban_db.py:2914, 3041`: raw `signal.SIGKILL` → `getattr(signal, "SIGKILL", signal.SIGTERM)` fallback (matches the pattern already used in `gateway/status.py`). ### OSError widening on `os.kill(pid, 0)` probes Windows raises `OSError` (WinError 87) for a gone PID instead of `ProcessLookupError`. Widened the catch at: - `gateway/run.py:15101` (`--replace` wait-for-exit loop — without this, the loop busy-spins the full 10s every Windows gateway start) - `hermes_cli/gateway.py:228, 460, 940` - `hermes_cli/profiles.py:777` - `tools/process_registry.py::_is_host_pid_alive` - `tools/browser_tool.py:1170, 1206` ### Dashboard PTY graceful degradation `hermes_cli/pty_bridge.py` depends on `fcntl`/`termios`/`ptyprocess`, none of which exist on native Windows. Previously a Windows dashboard would crash on `import hermes_cli.web_server` because of a top-level import. Now: - `hermes_cli/web_server.py` wraps the pty_bridge import in `try/except ImportError` and sets `_PTY_BRIDGE_AVAILABLE=False`. - The `/api/pty` WebSocket handler returns a friendly "use WSL2 for this tab" message instead of exploding. - Every other dashboard feature (sessions, jobs, metrics, config editor) runs natively on Windows. ### Dependency - `pyproject.toml`: add `tzdata>=2023.3; sys_platform == 'win32'` so Python's `zoneinfo` works on Windows (which has no IANA tzdata shipped with the OS). Credits @sprmn24 (PR #13182). ### Docs - README.md: removed "Native Windows is not supported"; added PowerShell one-liner and Git-for-Windows prerequisite note. - `website/docs/getting-started/installation.md`: new Windows section with capability matrix (everything native except the dashboard `/chat` PTY tab, which is WSL2-only). - `website/docs/user-guide/windows-wsl-quickstart.md`: reframed as "WSL2 as an alternative to native" rather than "the only way". - `website/docs/developer-guide/contributing.md`: updated cross-platform guidance with the `signal.SIGKILL` / `OSError` rules we enforce now. - `website/docs/user-guide/features/web-dashboard.md`: acknowledged native Windows works for everything except the embedded PTY pane. ## Why this shape Pulled from a survey of how other agent codebases handle native Windows (Claude Code, OpenCode, Codex, Cline): - All four treat Git Bash as the canonical shell on Windows, same as hermes already does in `tools/environments/local.py::_find_bash()`. - None of them force `SetConsoleOutputCP` — but they don't have to, Node/Rust write UTF-16 to the Win32 console API. Python does not get that for free, so we flip CP_UTF8 via ctypes. - None of them ship PowerShell-as-primary-shell (Claude Code exposes PS as a secondary tool; scope creep for this PR). - All of them use `taskkill /T /F` for force-kill on Windows, which is exactly what `gateway.status.terminate_pid(force=True)` does. ## Non-goals (deliberate scope limits) - No PowerShell-as-a-second-shell tool — worth designing separately. - No terminal routing rewrite (#12317, #15461, #19800 cluster) — that's the hardest design call and needs a separate doc. - No wholesale `open()` → `open(..., encoding="utf-8")` sweep (Tianworld cluster) — will do as follow-up if users hit actual breakage; most modern code already specifies it. ## Validation - 28 new tests in `tests/tools/test_windows_native_support.py` — all platform-mocked, pass on Linux CI. Cover: - `configure_windows_stdio` idempotency, opt-out, env-preservation - `terminate_pid` taskkill routing, failure → OSError, FileNotFoundError fallback - `getattr(signal, "SIGKILL", …)` fallback shape - `_is_host_pid_alive` OSError widening (Windows-gone-PID behavior) - Source-level checks that all entry points call `configure_windows_stdio` - pty_bridge import-guard present in `web_server.py` - README no longer says "not supported" - 12 pre-existing tests in `tests/tools/test_windows_compat.py` still pass. - `tests/hermes_cli/` ran fully (3909 passed, 9 failures — all confirmed pre-existing on main by stash-test). - `tests/gateway/` ran fully (5021 passed, 1 pre-existing failure). - `tests/tools/test_process_registry.py` + `test_browser_*` pass. - Manual smoke: `import hermes_cli.stdio; import gateway.run; import hermes_cli.web_server` — all clean, `_PTY_BRIDGE_AVAILABLE=True` on Linux (as expected). ## Files - New: `hermes_cli/stdio.py`, `tests/tools/test_windows_native_support.py` - Modified: `cli.py`, `gateway/run.py`, `hermes_cli/main.py`, `hermes_cli/profiles.py`, `hermes_cli/gateway.py`, `hermes_cli/kanban_db.py`, `hermes_cli/pty_bridge.py`, `hermes_cli/web_server.py`, `tools/browser_tool.py`, `tools/process_registry.py`, `pyproject.toml`, `README.md`, and 4 docs pages. Credits to everyone whose prior PR work informed these fixes — see the co-author trailers. All of the PRs listed in `~/.hermes/plans/windows-support-prs.md` fixing `os.kill` / `signal.SIGKILL` / UTF-8 stdio / tzdata / README patterns found the same issues; this PR consolidates them. Co-authored-by: Philip D'Souza <9472774+PhilipAD@users.noreply.github.com> Co-authored-by: Arecanon <42595053+ArecaNon@users.noreply.github.com> Co-authored-by: XiaoXiao0221 <263113677+XiaoXiao0221@users.noreply.github.com> Co-authored-by: Lars Hagen <1360677+lars-hagen@users.noreply.github.com> Co-authored-by: Luan Dias <65574834+luandiasrj@users.noreply.github.com> Co-authored-by: Ruzzgar <ruzzgarcn@gmail.com> Co-authored-by: sprmn24 <oncuevtv@gmail.com> Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com> Co-authored-by: Prasanna28Devadiga <54196612+Prasanna28Devadiga@users.noreply.github.com> |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| test_accretion_caps.py | ||
| test_ansi_strip.py | ||
| test_approval.py | ||
| test_approval_heartbeat.py | ||
| test_approval_plugin_hooks.py | ||
| test_base_environment.py | ||
| test_browser_camofox.py | ||
| test_browser_camofox_persistence.py | ||
| test_browser_camofox_state.py | ||
| test_browser_cdp_override.py | ||
| test_browser_cdp_tool.py | ||
| test_browser_chromium_check.py | ||
| test_browser_cleanup.py | ||
| test_browser_cloud_fallback.py | ||
| test_browser_console.py | ||
| test_browser_content_none_guard.py | ||
| test_browser_hardening.py | ||
| test_browser_homebrew_paths.py | ||
| test_browser_hybrid_routing.py | ||
| test_browser_lightpanda.py | ||
| test_browser_orphan_reaper.py | ||
| test_browser_secret_exfil.py | ||
| test_browser_ssrf_local.py | ||
| test_browser_supervisor.py | ||
| test_browser_supervisor_healthcheck.py | ||
| test_budget_config.py | ||
| test_checkpoint_manager.py | ||
| test_clarify_tool.py | ||
| test_clipboard.py | ||
| test_code_execution.py | ||
| test_code_execution_modes.py | ||
| test_command_guards.py | ||
| test_config_null_guard.py | ||
| test_credential_files.py | ||
| test_credential_pool_env_fallback.py | ||
| test_cron_approval_mode.py | ||
| test_cron_prompt_injection.py | ||
| test_cronjob_tools.py | ||
| test_daytona_environment.py | ||
| test_debug_helpers.py | ||
| test_delegate.py | ||
| test_delegate_composite_toolsets.py | ||
| test_delegate_subagent_timeout_diagnostic.py | ||
| test_delegate_toolset_scope.py | ||
| test_discord_tool.py | ||
| test_docker_environment.py | ||
| test_docker_find.py | ||
| test_dockerfile_node_modules_perms.py | ||
| test_dockerfile_pid1_reaping.py | ||
| test_env_passthrough.py | ||
| test_feishu_tools.py | ||
| test_file_operations.py | ||
| test_file_operations_edge_cases.py | ||
| test_file_ops_cwd_tracking.py | ||
| test_file_read_guards.py | ||
| test_file_staleness.py | ||
| test_file_state_registry.py | ||
| test_file_sync.py | ||
| test_file_sync_back.py | ||
| test_file_sync_perf.py | ||
| test_file_tools.py | ||
| test_file_tools_container_config.py | ||
| test_file_tools_live.py | ||
| test_file_write_safety.py | ||
| test_force_dangerous_override.py | ||
| test_fuzzy_match.py | ||
| test_hardline_blocklist.py | ||
| test_heartbeat_stale_thresholds.py | ||
| test_hidden_dir_filter.py | ||
| test_homeassistant_tool.py | ||
| test_image_generation.py | ||
| test_image_generation_env.py | ||
| test_image_generation_plugin_dispatch.py | ||
| test_init_session_cwd_respect.py | ||
| test_interrupt.py | ||
| test_kanban_tools.py | ||
| test_llm_content_none_guard.py | ||
| test_local_background_child_hang.py | ||
| test_local_env_blocklist.py | ||
| test_local_env_cwd_recovery.py | ||
| test_local_interrupt_cleanup.py | ||
| test_local_shell_init.py | ||
| test_local_tempdir.py | ||
| test_managed_browserbase_and_modal.py | ||
| test_managed_media_gateways.py | ||
| test_managed_modal_environment.py | ||
| test_managed_server_tool_support.py | ||
| test_managed_tool_gateway.py | ||
| test_mcp_cancelled_error_propagation.py | ||
| test_mcp_circuit_breaker.py | ||
| test_mcp_dynamic_discovery.py | ||
| test_mcp_empty_error_message.py | ||
| test_mcp_image_content.py | ||
| test_mcp_oauth.py | ||
| test_mcp_oauth_bidirectional.py | ||
| test_mcp_oauth_cold_load_expiry.py | ||
| test_mcp_oauth_integration.py | ||
| test_mcp_oauth_manager.py | ||
| test_mcp_oauth_metadata.py | ||
| test_mcp_probe.py | ||
| test_mcp_reconnect_signal.py | ||
| test_mcp_sse_transport.py | ||
| test_mcp_stability.py | ||
| test_mcp_structured_content.py | ||
| test_mcp_tool.py | ||
| test_mcp_tool_401_handling.py | ||
| test_mcp_tool_issue_948.py | ||
| test_mcp_tool_session_expired.py | ||
| test_mcp_utility_capability_gating.py | ||
| test_memory_tool.py | ||
| test_memory_tool_import_fallback.py | ||
| test_memory_tool_schema.py | ||
| test_mixture_of_agents_tool.py | ||
| test_modal_bulk_upload.py | ||
| test_modal_sandbox_fixes.py | ||
| test_modal_snapshot_isolation.py | ||
| test_notify_on_complete.py | ||
| test_osv_check.py | ||
| test_parse_env_var.py | ||
| test_patch_parser.py | ||
| test_process_registry.py | ||
| test_read_loop_detection.py | ||
| test_registry.py | ||
| test_resolve_path.py | ||
| test_rl_training_tool.py | ||
| test_schema_sanitizer.py | ||
| test_search_hidden_dirs.py | ||
| test_send_message_missing_platforms.py | ||
| test_send_message_tool.py | ||
| test_session_search.py | ||
| test_shared_container_task_id.py | ||
| test_signal_media.py | ||
| test_singularity_preflight.py | ||
| test_skill_env_passthrough.py | ||
| test_skill_improvements.py | ||
| test_skill_manager_tool.py | ||
| test_skill_provenance.py | ||
| test_skill_size_limits.py | ||
| test_skill_usage.py | ||
| test_skill_view_path_check.py | ||
| test_skill_view_traversal.py | ||
| test_skills_guard.py | ||
| test_skills_hub.py | ||
| test_skills_hub_clawhub.py | ||
| test_skills_sync.py | ||
| test_skills_tool.py | ||
| test_slash_confirm.py | ||
| test_spotify_client.py | ||
| test_ssh_bulk_upload.py | ||
| test_ssh_environment.py | ||
| test_symlink_prefix_confusion.py | ||
| test_sync_back_backends.py | ||
| test_terminal_compound_background.py | ||
| test_terminal_config_env_sync.py | ||
| test_terminal_exit_semantics.py | ||
| test_terminal_foreground_timeout_cap.py | ||
| test_terminal_none_command_guard.py | ||
| test_terminal_output_transform_hook.py | ||
| test_terminal_requirements.py | ||
| test_terminal_timeout_output.py | ||
| test_terminal_tool.py | ||
| test_terminal_tool_pty_fallback.py | ||
| test_terminal_tool_requirements.py | ||
| test_threaded_process_handle.py | ||
| test_tirith_security.py | ||
| test_todo_tool.py | ||
| test_tool_backend_helpers.py | ||
| test_tool_call_parsers.py | ||
| test_tool_output_limits.py | ||
| test_tool_result_storage.py | ||
| test_transcription.py | ||
| test_transcription_dotenv_fallback.py | ||
| test_transcription_tools.py | ||
| test_tts_command_providers.py | ||
| test_tts_dotenv_fallback.py | ||
| test_tts_gemini.py | ||
| test_tts_kittentts.py | ||
| test_tts_max_text_length.py | ||
| test_tts_mistral.py | ||
| test_tts_piper.py | ||
| test_tts_speed.py | ||
| test_url_safety.py | ||
| test_vercel_sandbox_environment.py | ||
| test_video_analyze.py | ||
| test_vision_tools.py | ||
| test_voice_cli_integration.py | ||
| test_voice_mode.py | ||
| test_watch_patterns.py | ||
| test_web_providers.py | ||
| test_web_providers_brave_free.py | ||
| test_web_providers_ddgs.py | ||
| test_web_providers_searxng.py | ||
| test_web_tools_config.py | ||
| test_web_tools_tavily.py | ||
| test_website_policy.py | ||
| test_windows_compat.py | ||
| test_windows_native_support.py | ||
| test_write_deny.py | ||
| test_yolo_mode.py | ||
| test_zombie_process_cleanup.py | ||