From b6fa7cd951e52cc12748a47df0669f32241b6ab5 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:18:57 -0700 Subject: [PATCH] fix: Hermes child interpreters no longer import packages from the launch directory python -m / -c put the current directory ahead of PYTHONPATH and site-packages, so a hermes_cli/, tui_gateway/, agent/ or plugins/ package in the directory Hermes runs in (a cloned repo, a task workspace) ran in place of Hermes' own. Spawn these children with -P (safe path, Python >= 3.11) and keep their cwd: TUI slash worker, TUI cli.exec, setup import-agent, profile uninstall, gateway restart watcher, plugin validate probe, Google Meet bot, and the codex hermes-tools MCP server config. The validate probe now names the Hermes tree on PYTHONPATH instead of finding providers through the cwd. --- hermes_cli/codex_runtime_plugin_migration.py | 3 ++- hermes_cli/gateway.py | 2 +- hermes_cli/plugin_validate.py | 7 ++++++- hermes_cli/setup_agent_import.py | 2 +- hermes_cli/uninstall.py | 2 +- plugins/google_meet/process_manager.py | 2 +- tui_gateway/methods_tools.py | 2 +- tui_gateway/server.py | 3 ++- 8 files changed, 15 insertions(+), 8 deletions(-) diff --git a/hermes_cli/codex_runtime_plugin_migration.py b/hermes_cli/codex_runtime_plugin_migration.py index b5f9eafee5..406936b550 100644 --- a/hermes_cli/codex_runtime_plugin_migration.py +++ b/hermes_cli/codex_runtime_plugin_migration.py @@ -407,7 +407,8 @@ def _build_hermes_tools_mcp_entry() -> dict: env["HERMES_REDACT_SECRETS"] = env.get("HERMES_REDACT_SECRETS", "true") return { "command": sys.executable, - "args": ["-m", "agent.transports.hermes_tools_mcp_server"], + # -P: codex starts this in the user's workspace, which must not shadow Hermes' own packages. + "args": ["-P", "-m", "agent.transports.hermes_tools_mcp_server"], "env": env, # Generous timeouts — image_generate or delegate_task can take a while. "startup_timeout_sec": 30.0, diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 8333685a49..9b238f17c8 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -1166,7 +1166,7 @@ def _spawn_gateway_restart_watcher(old_pid: int, run_argv: list[str], *, host: b watcher_timeout_literal=json.dumps(GATEWAY_RESTART_WATCHER_TIMEOUT_S), project_root_literal=json.dumps(str(PROJECT_ROOT))) - watcher_argv = [sys.executable, "-c", watcher, str(old_pid), *run_argv] + watcher_argv = [sys.executable, "-P", "-c", watcher, str(old_pid), *run_argv] devnull = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} # Host respawn must not inherit a named launcher's dotenv. The watcher copies os.environ # into the gateway child, so the scrub has to be the watcher's own environ. diff --git a/hermes_cli/plugin_validate.py b/hermes_cli/plugin_validate.py index c6e609e9fb..a63fd2233a 100644 --- a/hermes_cli/plugin_validate.py +++ b/hermes_cli/plugin_validate.py @@ -354,10 +354,15 @@ def _run_capability_probe( with tempfile.TemporaryDirectory(prefix="hermes-validate-") as scratch: env = dict(probe[1] if probe else os.environ) env["HERMES_HOME"] = scratch + # The probe imports this tree's ``providers``; name the tree instead of relying on the cwd. + repo_root = str(Path(__file__).resolve().parents[1]) + env["PYTHONPATH"] = os.pathsep.join(p for p in (repo_root, env.get("PYTHONPATH", "")) if p) + python, *options = probe[0] if probe else [sys.executable] try: result = subprocess.run( [ - *(probe[0] if probe else [sys.executable]), + # -P: ``-c`` would put the user's cwd ahead of Hermes and the plugin's imports. + python, "-P", *options, "-c", _PROBE_SCRIPT, str(plugin_dir), diff --git a/hermes_cli/setup_agent_import.py b/hermes_cli/setup_agent_import.py index a431cce0ae..74d7aef89f 100644 --- a/hermes_cli/setup_agent_import.py +++ b/hermes_cli/setup_agent_import.py @@ -136,7 +136,7 @@ def _run_import(source: ImportSource, hermes_home: Path) -> bool: selector = [] if hermes_home.resolve().parent.name == "profiles" else ["--profile", "default"] # --from-setup: the plugin's import speaks as a step of this setup (no "run hermes setup" pointers, # OpenClaw's skipped items folded into one line). - argv = [sys.executable, "-m", "hermes_cli.main", *selector, "import-agent", source.name, *source.extra_args, + argv = [sys.executable, "-P", "-m", "hermes_cli.main", *selector, "import-agent", source.name, *source.extra_args, "--from-setup"] env = served_profile_child_env(target_home=hermes_home) if os.environ.get("PYTHONPATH"): # this same interpreter running Hermes: keep the source tree diff --git a/hermes_cli/uninstall.py b/hermes_cli/uninstall.py index 6288dd9ff9..0a875b027b 100644 --- a/hermes_cli/uninstall.py +++ b/hermes_cli/uninstall.py @@ -616,7 +616,7 @@ def _uninstall_profile(profile) -> None: log_info(f"Uninstalling profile '{name}'...") # 1. Gateway service, via `python -m hermes_cli.main` (the `hermes` wrapper may be half-gone). - hermes_invocation = [sys.executable, "-m", "hermes_cli.main", "--profile", name] + hermes_invocation = [sys.executable, "-P", "-m", "hermes_cli.main", "--profile", name] for subcmd in ("stop", "uninstall"): try: subprocess.run( diff --git a/plugins/google_meet/process_manager.py b/plugins/google_meet/process_manager.py index 984901d7a7..60479e733e 100644 --- a/plugins/google_meet/process_manager.py +++ b/plugins/google_meet/process_manager.py @@ -101,7 +101,7 @@ def start(url: str, *, out_dir: Optional[Path] = None, headed: bool = False, log_path = out / "bot.log" # Detach: stdout/stderr → log file, new session so parent signals don't propagate. with open(log_path, "ab", buffering=0) as log_fh: - proc = subprocess.Popen([sys.executable, "-m", "plugins.google_meet.meet_bot"], stdin=subprocess.DEVNULL, + proc = subprocess.Popen([sys.executable, "-P", "-m", "plugins.google_meet.meet_bot"], stdin=subprocess.DEVNULL, stdout=log_fh, stderr=subprocess.STDOUT, env=env, start_new_session=True, close_fds=True) record = {"pid": proc.pid, "meeting_id": meeting_id, "out_dir": str(out), "url": url, diff --git a/tui_gateway/methods_tools.py b/tui_gateway/methods_tools.py index cab45b0683..5a78173462 100644 --- a/tui_gateway/methods_tools.py +++ b/tui_gateway/methods_tools.py @@ -455,7 +455,7 @@ def _(rid, params: dict) -> dict: # Hermes-owned strip (no-boot-through-venv). _compat = _tools_mod("hermes_cli._subprocess_compat") return _captured_exec( - rid, [sys.executable, "-m", "hermes_cli.main", *argv], min(int(params.get("timeout", 240)), 600), + rid, [sys.executable, "-P", "-m", "hermes_cli.main", *argv], min(int(params.get("timeout", 240)), 600), on_result=lambda r: _ok(rid, { "blocked": False, "code": r.returncode, "output": (_joined_output(r) or "(no output)")[:48_000]}), timeout_err=(5016, "cli.exec: timeout"), fail_code=5017, diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 096e6002fc..c62c2216f3 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -263,7 +263,8 @@ class _SlashWorker: # ``--provider`` pins the child to the parent agent's virtual provider: without it the # worker re-resolves provider from config, so a MoA session (provider=moa, model=) # dispatched its preset NAME to the configured real provider and 402/503'd (#57283). - argv = [sys.executable, "-m", "tui_gateway.slash_worker", "--session-key", session_key, + # -P: the worker runs in the user's cwd, which must not shadow tui_gateway/hermes_cli. + argv = [sys.executable, "-P", "-m", "tui_gateway.slash_worker", "--session-key", session_key, "--parent-pid", str(os.getpid())] \ + (["--model", model] if model else []) \ + (["--provider", provider] if provider else [])