mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-30 11:52:04 +00:00
fix(windows): cover remaining console-flash spawn legs (#54417)
This commit is contained in:
parent
b31b0b9d95
commit
9a0010fd46
12 changed files with 234 additions and 22 deletions
|
|
@ -5501,6 +5501,19 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
# run as a self-restart loop guard and the gateway stays stopped.
|
||||
watcher_env.pop("_HERMES_GATEWAY", None)
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
watcher_python = sys.executable
|
||||
try:
|
||||
# Prefer a real GUI-subsystem interpreter for the watcher
|
||||
# itself. With uv venvs, ``python.exe`` can re-exec the base
|
||||
# console interpreter and flash even when the Popen carries
|
||||
# CREATE_NO_WINDOW; pythonw.exe avoids console allocation.
|
||||
from hermes_cli.gateway_windows import _resolve_detached_python
|
||||
|
||||
watcher_python, _watcher_venv_dir, _watcher_site_packages = (
|
||||
_resolve_detached_python(sys.executable)
|
||||
)
|
||||
except Exception:
|
||||
watcher_python = sys.executable
|
||||
venv_dir = Path(watcher_env.get("VIRTUAL_ENV") or project_root / "venv")
|
||||
site_packages = venv_dir / "Lib" / "site-packages"
|
||||
if site_packages.exists():
|
||||
|
|
@ -5510,7 +5523,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
pythonpath.append(watcher_env["PYTHONPATH"])
|
||||
watcher_env["PYTHONPATH"] = os.pathsep.join(dict.fromkeys(pythonpath))
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-c", watcher, str(current_pid), str(restart_after_s), *cmd_argv],
|
||||
[watcher_python, "-c", watcher, str(current_pid), str(restart_after_s), *cmd_argv],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
env=watcher_env,
|
||||
|
|
|
|||
|
|
@ -171,17 +171,18 @@ def _read_process_cmdline(pid: int) -> Optional[str]:
|
|||
if raw:
|
||||
return raw.replace(b"\x00", b" ").decode("utf-8", errors="ignore").strip()
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ps", "-p", str(pid), "-o", "command="],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
if not _IS_WINDOWS:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ps", "-p", str(pid), "-o", "command="],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
|
||||
# Windows fallback: psutil (already used by _pid_exists)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -180,7 +180,11 @@ def _get_parent_pid(pid: int) -> int | None:
|
|||
pass
|
||||
except Exception:
|
||||
return None
|
||||
# Fallback: shell out to ps (POSIX only — bare ``ps`` doesn't exist on Windows).
|
||||
# Fallback: shell out to ps (POSIX only). Git Bash installs ``ps.exe`` on
|
||||
# Windows; running it from the windowless desktop/gateway backend flashes a
|
||||
# console, and psutil above is the authoritative Windows path anyway.
|
||||
if is_windows():
|
||||
return None
|
||||
if not shutil.which("ps"):
|
||||
return None
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -317,11 +317,14 @@ def node_tool_runnable(path: str | None) -> bool:
|
|||
import subprocess
|
||||
|
||||
try:
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
result = subprocess.run(
|
||||
[path, "--version"],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
env=with_hermes_node_path(),
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired, ValueError):
|
||||
return False
|
||||
|
|
@ -562,11 +565,14 @@ def agent_browser_runnable(path: str | None) -> bool:
|
|||
import subprocess
|
||||
|
||||
try:
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
result = subprocess.run(
|
||||
[path, "--version"],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
env=with_hermes_node_path(),
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired, ValueError):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -353,6 +353,49 @@ async def test_windows_detached_restart_scrubs_gateway_marker(monkeypatch, tmp_p
|
|||
assert kwargs["stderr"] is subprocess.DEVNULL
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_windows_detached_restart_uses_pythonw_for_watcher(monkeypatch, tmp_path):
|
||||
runner, _adapter = make_restart_runner()
|
||||
popen_calls = []
|
||||
venv_dir = tmp_path / "venv"
|
||||
site_packages = venv_dir / "Lib" / "site-packages"
|
||||
site_packages.mkdir(parents=True)
|
||||
|
||||
monkeypatch.setattr(gateway_run.sys, "platform", "win32")
|
||||
monkeypatch.setattr(gateway_run.sys, "executable", r"C:\venv\Scripts\python.exe")
|
||||
monkeypatch.setattr(gateway_run, "_resolve_hermes_bin", lambda: ["hermes"])
|
||||
monkeypatch.setattr(gateway_run.os, "getpid", lambda: 321)
|
||||
monkeypatch.setenv("VIRTUAL_ENV", str(venv_dir))
|
||||
|
||||
import hermes_cli._subprocess_compat as subprocess_compat
|
||||
import hermes_cli.gateway_windows as gateway_windows
|
||||
|
||||
monkeypatch.setattr(
|
||||
gateway_windows,
|
||||
"_resolve_detached_python",
|
||||
lambda _python: (r"C:\Python311\pythonw.exe", venv_dir, [str(site_packages)]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
subprocess_compat,
|
||||
"windows_detach_popen_kwargs",
|
||||
lambda: {"creationflags": 0x08000008},
|
||||
)
|
||||
|
||||
def fake_popen(cmd, **kwargs):
|
||||
popen_calls.append((cmd, kwargs))
|
||||
return MagicMock()
|
||||
|
||||
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
||||
|
||||
await runner._launch_detached_restart_command()
|
||||
|
||||
assert len(popen_calls) == 1
|
||||
cmd, kwargs = popen_calls[0]
|
||||
assert cmd[0] == r"C:\Python311\pythonw.exe"
|
||||
assert cmd[-3:] == ["hermes", "gateway", "restart"]
|
||||
assert kwargs["creationflags"] == 0x08000008
|
||||
|
||||
|
||||
# ── Shutdown notification tests ──────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
|
@ -1351,6 +1352,7 @@ class TestReadProcessCmdlinePsFallback:
|
|||
|
||||
def test_ps_fallback_when_proc_unavailable(self, monkeypatch):
|
||||
monkeypatch.setattr(status.Path, "read_bytes", lambda self: (_ for _ in ()).throw(FileNotFoundError))
|
||||
monkeypatch.setattr(status, "_IS_WINDOWS", False)
|
||||
monkeypatch.setattr(
|
||||
status.subprocess, "run",
|
||||
lambda args, **kwargs: SimpleNamespace(returncode=0, stdout="/usr/libexec/bluetoothuserd\n"),
|
||||
|
|
@ -1360,6 +1362,7 @@ class TestReadProcessCmdlinePsFallback:
|
|||
|
||||
def test_ps_fallback_returns_none_on_failure(self, monkeypatch):
|
||||
monkeypatch.setattr(status.Path, "read_bytes", lambda self: (_ for _ in ()).throw(FileNotFoundError))
|
||||
monkeypatch.setattr(status, "_IS_WINDOWS", False)
|
||||
monkeypatch.setattr(
|
||||
status.subprocess, "run",
|
||||
lambda args, **kwargs: SimpleNamespace(returncode=1, stdout=""),
|
||||
|
|
@ -1381,6 +1384,7 @@ class TestReadProcessCmdlinePsFallback:
|
|||
|
||||
def test_ps_fallback_used_when_proc_returns_empty(self, monkeypatch):
|
||||
monkeypatch.setattr(status.Path, "read_bytes", lambda self: b"")
|
||||
monkeypatch.setattr(status, "_IS_WINDOWS", False)
|
||||
monkeypatch.setattr(
|
||||
status.subprocess, "run",
|
||||
lambda args, **kwargs: SimpleNamespace(returncode=0, stdout="python hermes_cli/main.py gateway run\n"),
|
||||
|
|
@ -1388,6 +1392,34 @@ class TestReadProcessCmdlinePsFallback:
|
|||
result = status._read_process_cmdline(12345)
|
||||
assert "hermes_cli/main.py" in result
|
||||
|
||||
def test_windows_skips_ps_fallback_and_uses_psutil(self, monkeypatch):
|
||||
monkeypatch.setattr(status.Path, "read_bytes", lambda self: (_ for _ in ()).throw(FileNotFoundError))
|
||||
monkeypatch.setattr(status, "_IS_WINDOWS", True)
|
||||
ps_calls = []
|
||||
monkeypatch.setattr(
|
||||
status.subprocess,
|
||||
"run",
|
||||
lambda args, **kwargs: ps_calls.append((args, kwargs)) or SimpleNamespace(returncode=0, stdout="ps should not run\n"),
|
||||
)
|
||||
|
||||
class _Proc:
|
||||
def __init__(self, pid):
|
||||
self.pid = pid
|
||||
|
||||
def cmdline(self):
|
||||
return ["pythonw.exe", "-m", "hermes_cli.main", "gateway", "run"]
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"psutil",
|
||||
SimpleNamespace(Process=_Proc),
|
||||
)
|
||||
|
||||
result = status._read_process_cmdline(12345)
|
||||
|
||||
assert result == "pythonw.exe -m hermes_cli.main gateway run"
|
||||
assert ps_calls == []
|
||||
|
||||
|
||||
class TestCorruptStatusFiles:
|
||||
"""A status / pid file holding non-UTF-8 (binary) bytes must read as
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -611,6 +612,43 @@ class TestAgentBrowserRunnable:
|
|||
assert agent_browser_runnable("npx agent-browser") is True
|
||||
assert agent_browser_runnable("/usr/local/bin/npx agent-browser") is True
|
||||
|
||||
def test_version_probe_uses_windows_hide_flags(self, tmp_path, monkeypatch):
|
||||
good = self._stub(tmp_path, "agent-browser", "#!/bin/sh\necho hi\n")
|
||||
captured = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured.append((cmd, kwargs))
|
||||
return SimpleNamespace(returncode=0)
|
||||
|
||||
import hermes_cli._subprocess_compat as subprocess_compat
|
||||
import subprocess as subprocess_mod
|
||||
|
||||
monkeypatch.setattr(subprocess_compat, "windows_hide_flags", lambda: 0x08000000)
|
||||
monkeypatch.setattr(subprocess_mod, "run", fake_run)
|
||||
|
||||
assert agent_browser_runnable(str(good)) is True
|
||||
assert captured[0][0] == [str(good), "--version"]
|
||||
assert captured[0][1]["creationflags"] == 0x08000000
|
||||
|
||||
|
||||
def test_node_tool_probe_uses_windows_hide_flags(self, tmp_path, monkeypatch):
|
||||
good = self._stub(tmp_path, "node", "#!/bin/sh\necho v22\n")
|
||||
captured = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured.append((cmd, kwargs))
|
||||
return SimpleNamespace(returncode=0)
|
||||
|
||||
import hermes_cli._subprocess_compat as subprocess_compat
|
||||
import subprocess as subprocess_mod
|
||||
|
||||
monkeypatch.setattr(subprocess_compat, "windows_hide_flags", lambda: 0x08000000)
|
||||
monkeypatch.setattr(subprocess_mod, "run", fake_run)
|
||||
|
||||
assert node_tool_runnable(str(good)) is True
|
||||
assert captured[0][0] == [str(good), "--version"]
|
||||
assert captured[0][1]["creationflags"] == 0x08000000
|
||||
|
||||
|
||||
class TestGetHermesDir:
|
||||
"""Tests for ``get_hermes_dir(new_subpath, old_name)``.
|
||||
|
|
|
|||
|
|
@ -299,3 +299,29 @@ def test_local_stt_audio_prep_hides_ffmpeg_window(monkeypatch, tmp_path):
|
|||
|
||||
assert captured[0][0][0] == "ffmpeg"
|
||||
assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW
|
||||
|
||||
def test_tui_slash_worker_hides_python_window(monkeypatch):
|
||||
from tui_gateway import server
|
||||
|
||||
captured = []
|
||||
|
||||
class _Proc:
|
||||
stdin = SimpleNamespace()
|
||||
stdout = []
|
||||
stderr = []
|
||||
|
||||
def fake_popen(cmd, **kwargs):
|
||||
captured.append((cmd, kwargs))
|
||||
return _Proc()
|
||||
|
||||
monkeypatch.setattr(server.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(server.threading, "Thread", lambda *a, **k: SimpleNamespace(start=lambda: None))
|
||||
|
||||
import hermes_cli._subprocess_compat as subprocess_compat
|
||||
|
||||
monkeypatch.setattr(subprocess_compat, "windows_hide_flags", lambda: _CREATE_NO_WINDOW)
|
||||
|
||||
server._SlashWorker("session-key", "model-x")
|
||||
|
||||
assert captured[0][0][:3] == [server.sys.executable, "-m", "tui_gateway.slash_worker"]
|
||||
assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class TestCheckBrowserRequirementsChromium:
|
|||
|
||||
def test_local_mode_with_chromium_returns_true(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(bt, "_is_camofox_mode", lambda: False)
|
||||
monkeypatch.setattr(bt, "_find_agent_browser", lambda: "/usr/local/bin/agent-browser")
|
||||
monkeypatch.setattr(bt, "_find_agent_browser", lambda **_kw: "/usr/local/bin/agent-browser")
|
||||
monkeypatch.setattr(bt, "_requires_real_termux_browser_install", lambda _: False)
|
||||
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: None)
|
||||
monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", str(tmp_path))
|
||||
|
|
@ -93,7 +93,7 @@ class TestCheckBrowserRequirementsChromium:
|
|||
return "browserbase"
|
||||
|
||||
monkeypatch.setattr(bt, "_is_camofox_mode", lambda: False)
|
||||
monkeypatch.setattr(bt, "_find_agent_browser", lambda: "/usr/local/bin/agent-browser")
|
||||
monkeypatch.setattr(bt, "_find_agent_browser", lambda **_kw: "/usr/local/bin/agent-browser")
|
||||
monkeypatch.setattr(bt, "_requires_real_termux_browser_install", lambda _: False)
|
||||
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: FakeProvider())
|
||||
# Point chromium search at an empty dir — should not matter for cloud.
|
||||
|
|
@ -102,6 +102,23 @@ class TestCheckBrowserRequirementsChromium:
|
|||
|
||||
assert bt.check_browser_requirements() is True
|
||||
|
||||
def test_startup_check_uses_lightweight_agent_browser_lookup(self, monkeypatch, tmp_path):
|
||||
seen = []
|
||||
|
||||
def fake_find_agent_browser(**kwargs):
|
||||
seen.append(kwargs)
|
||||
return "/usr/local/bin/agent-browser"
|
||||
|
||||
monkeypatch.setattr(bt, "_is_camofox_mode", lambda: False)
|
||||
monkeypatch.setattr(bt, "_find_agent_browser", fake_find_agent_browser)
|
||||
monkeypatch.setattr(bt, "_requires_real_termux_browser_install", lambda _: False)
|
||||
monkeypatch.setattr(bt, "_get_cloud_provider", lambda: None)
|
||||
monkeypatch.setenv("PLAYWRIGHT_BROWSERS_PATH", str(tmp_path))
|
||||
(tmp_path / "chromium-1208").mkdir()
|
||||
|
||||
assert bt.check_browser_requirements() is True
|
||||
assert seen == [{"validate": False}]
|
||||
|
||||
def test_camofox_mode_does_not_require_chromium(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(bt, "_is_camofox_mode", lambda: True)
|
||||
# Even with no chromium on disk, camofox drives its own backend.
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ class TestBrowserRequirements:
|
|||
monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
|
||||
monkeypatch.setattr("tools.browser_tool._is_camofox_mode", lambda: False)
|
||||
monkeypatch.setattr("tools.browser_tool._get_cloud_provider", lambda: None)
|
||||
monkeypatch.setattr("tools.browser_tool._find_agent_browser", lambda: "npx agent-browser")
|
||||
monkeypatch.setattr("tools.browser_tool._find_agent_browser", lambda **_kw: "npx agent-browser")
|
||||
|
||||
assert check_browser_requirements() is False
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ class TestRunBrowserCommandTermuxFallback:
|
|||
def test_termux_local_mode_rejects_bare_npx_fallback(self, monkeypatch):
|
||||
monkeypatch.setenv("TERMUX_VERSION", "0.118.3")
|
||||
monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr")
|
||||
monkeypatch.setattr("tools.browser_tool._find_agent_browser", lambda: "npx agent-browser")
|
||||
monkeypatch.setattr("tools.browser_tool._find_agent_browser", lambda **_kw: "npx agent-browser")
|
||||
monkeypatch.setattr("tools.browser_tool._get_cloud_provider", lambda: None)
|
||||
|
||||
result = _run_browser_command("task-1", "navigate", ["https://example.com"])
|
||||
|
|
|
|||
|
|
@ -2002,7 +2002,15 @@ def _get_session_info(task_id: Optional[str] = None) -> Dict[str, str]:
|
|||
|
||||
|
||||
|
||||
def _find_agent_browser() -> str:
|
||||
def _agent_browser_candidate_present(path: str | None) -> bool:
|
||||
if not path:
|
||||
return False
|
||||
if " " in path and path.split()[0].endswith("npx"):
|
||||
return True
|
||||
return os.path.exists(path) and (os.name == "nt" or os.access(path, os.X_OK))
|
||||
|
||||
|
||||
def _find_agent_browser(*, validate: bool = True) -> str:
|
||||
"""
|
||||
Find the agent-browser CLI executable.
|
||||
|
||||
|
|
@ -2041,7 +2049,11 @@ def _find_agent_browser() -> str:
|
|||
|
||||
# Check if it's in PATH (global install)
|
||||
which_result = shutil.which("agent-browser")
|
||||
if which_result and agent_browser_runnable(which_result):
|
||||
if which_result and (
|
||||
agent_browser_runnable(which_result) if validate else _agent_browser_candidate_present(which_result)
|
||||
):
|
||||
if not validate:
|
||||
return which_result
|
||||
_cached_agent_browser = which_result
|
||||
_agent_browser_resolved = True
|
||||
return which_result
|
||||
|
|
@ -2051,7 +2063,11 @@ def _find_agent_browser() -> str:
|
|||
extended_path = _merge_browser_path("")
|
||||
if extended_path:
|
||||
which_result = shutil.which("agent-browser", path=extended_path)
|
||||
if which_result and agent_browser_runnable(which_result):
|
||||
if which_result and (
|
||||
agent_browser_runnable(which_result) if validate else _agent_browser_candidate_present(which_result)
|
||||
):
|
||||
if not validate:
|
||||
return which_result
|
||||
_cached_agent_browser = which_result
|
||||
_agent_browser_resolved = True
|
||||
return which_result
|
||||
|
|
@ -2068,7 +2084,11 @@ def _find_agent_browser() -> str:
|
|||
local_bin_dir = repo_root / "node_modules" / ".bin"
|
||||
if local_bin_dir.is_dir():
|
||||
local_which = shutil.which("agent-browser", path=str(local_bin_dir))
|
||||
if local_which and agent_browser_runnable(local_which):
|
||||
if local_which and (
|
||||
agent_browser_runnable(local_which) if validate else _agent_browser_candidate_present(local_which)
|
||||
):
|
||||
if not validate:
|
||||
return local_which
|
||||
_cached_agent_browser = local_which
|
||||
_agent_browser_resolved = True
|
||||
return _cached_agent_browser
|
||||
|
|
@ -2078,10 +2098,15 @@ def _find_agent_browser() -> str:
|
|||
if not npx_path and extended_path:
|
||||
npx_path = shutil.which("npx", path=extended_path)
|
||||
if npx_path:
|
||||
if not validate:
|
||||
return "npx agent-browser"
|
||||
_cached_agent_browser = "npx agent-browser"
|
||||
_agent_browser_resolved = True
|
||||
return _cached_agent_browser
|
||||
|
||||
if not validate:
|
||||
raise FileNotFoundError("agent-browser CLI not found")
|
||||
|
||||
# Nothing found — try lazy installation before giving up.
|
||||
try:
|
||||
from hermes_cli.dep_ensure import ensure_dependency
|
||||
|
|
@ -4206,8 +4231,12 @@ def check_browser_requirements() -> bool:
|
|||
return True
|
||||
|
||||
# The agent-browser CLI is required for local launch and cloud-provider flows.
|
||||
# Tool-schema assembly runs during Desktop startup; do not execute
|
||||
# ``agent-browser --version`` here, because Windows .cmd shims route through
|
||||
# cmd.exe and can flash a console before the user invokes any browser tool.
|
||||
# Actual browser execution paths still validate the candidate before use.
|
||||
try:
|
||||
browser_cmd = _find_agent_browser()
|
||||
browser_cmd = _find_agent_browser(validate=False)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,8 @@ class _SlashWorker:
|
|||
argv += ["--model", model]
|
||||
|
||||
self._closed = False
|
||||
from hermes_cli._subprocess_compat import windows_hide_flags
|
||||
|
||||
self.proc = subprocess.Popen(
|
||||
argv,
|
||||
stdin=subprocess.PIPE,
|
||||
|
|
@ -289,6 +291,7 @@ class _SlashWorker:
|
|||
# slash_worker runs the Hermes agent → needs provider credentials.
|
||||
# Tier-1 secrets (gateway/GitHub/infra) are still stripped (#29157).
|
||||
env=hermes_subprocess_env(inherit_credentials=True),
|
||||
creationflags=windows_hide_flags(),
|
||||
)
|
||||
threading.Thread(target=self._drain_stdout, daemon=True).start()
|
||||
threading.Thread(target=self._drain_stderr, daemon=True).start()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue