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.
This commit is contained in:
Teknium committed 2026-10-25 21:57:29 -07:00
1 parent 4d0d67ba38
commit b6fa7cd951
8 files changed
+15 -8

No files matched your search

+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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.
+6 -1
View File
@@ -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),
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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=<preset>)
# 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 [])