fix(tools): resolve hermes CLI beside the interpreter in bot_mode_dm deliveries

Bot-to-bot message_agent delivery builds both transport argvs (local
teammate chat and peer dm) with a bare "hermes" as argv[0]. Since #96631
the delivery runner spawns under terminal_tool's isolated host-local
environment, which does not inherit the gateway's PATH — so on
docker/service installs (venv at /opt/hermes/.venv) every delivery exits
with FileNotFoundError: 'hermes'.

Resolve the CLI with bot_relay._hermes_cli() (#93590) — the venv sibling
of this interpreter, then shutil.which, then the bare name — at both
argv construction sites. The turn-lock matcher in _delivery_lock()
already matches argv[0] by basename, so absolute paths lock exactly as
before.

Fixes #100662
This commit is contained in:
liuhao1024
2026-09-02 04:25:10 +08:00
committed by Teknium
parent 1f1f02e354
commit 37243bd668
2 changed files with 48 additions and 4 deletions

View File

@@ -16,7 +16,7 @@ from pathlib import Path
import pytest
from tools import bot_mode_dm, bot_mode_probe
from tools import bot_mode_dm, bot_mode_probe, bot_relay
@pytest.fixture(autouse=True)
@@ -234,6 +234,9 @@ def _runner_author(command):
def test_local_delivery_command_and_ack(tmp_path, monkeypatch):
calls = _capture_spawn(monkeypatch)
# These assertions target the -p/turn-args shape; pin the entrypoint resolution
# so the test stays hermetic across venvs that do/don't expose a sibling script.
monkeypatch.setattr(bot_relay, "_hermes_cli", lambda: "hermes")
home = _managed_home(tmp_path, teammates=("researcher",))
agent = _FakeAgent(home, title="Bot Chat")
@@ -295,6 +298,7 @@ def test_peer_delivery_command_pins_registry_profile_for_secondary_bots(
tool-side roster (read from the machine-root config) validated the
target."""
calls = _capture_spawn(monkeypatch)
monkeypatch.setattr(bot_relay, "_hermes_cli", lambda: "hermes")
home = _managed_home(tmp_path, peers=("spark",))
# A reviewer-profile gateway context: the agent's session db lives under
# that profile's home, so _agent_home() resolves there while the
@@ -316,6 +320,7 @@ def test_peer_delivery_command_pins_registry_profile_for_secondary_bots(
def test_peer_delivery_command(tmp_path, monkeypatch):
calls = _capture_spawn(monkeypatch)
monkeypatch.setattr(bot_relay, "_hermes_cli", lambda: "hermes")
monkeypatch.setattr("socket.gethostname", lambda: "eri-mac.local")
home = _managed_home(tmp_path, peers=("spark",))
agent = _FakeAgent(home, title="Bot Chat")
@@ -341,6 +346,40 @@ def test_peer_delivery_command(tmp_path, monkeypatch):
assert transport_argv == ["hermes", "-p", "default", "peer", "dm", "spark"]
def test_delivery_pins_the_hermes_entrypoint_beside_this_interpreter(tmp_path, monkeypatch):
"""A background delivery must not rely on PATH: the runner's service context
lacks the gateway's venv bin dir, so a bare ``hermes`` resolves to a system
install whose shebang picks the wrong interpreter and dies on import (#108628).
Both transports must invoke the entrypoint beside this interpreter instead."""
venv_bin = tmp_path / "venv" / ("Scripts" if sys.platform == "win32" else "bin")
venv_bin.mkdir(parents=True)
hermes_entry = venv_bin / ("hermes.exe" if sys.platform == "win32" else "hermes")
hermes_entry.write_text("#!/bin/sh\n", encoding="utf-8")
monkeypatch.setattr(sys, "executable", str(venv_bin / "python3"))
calls = _capture_spawn(monkeypatch)
home = _managed_home(tmp_path, teammates=("researcher",), peers=("spark",))
agent = _FakeAgent(home, title="Bot Chat")
result = json.loads(
bot_mode_dm.message_agent_tool(target="researcher", message="ping", agent=agent)
)
assert result["status"] == "sent"
mode, _dm_file, transport_argv = _runner_parts(calls[0]["command"])
assert mode == "query-file"
assert transport_argv[0] == str(hermes_entry)
assert transport_argv[1:] == ["-p", "researcher", "chat", "--in", "~", "-c", "Bot Chat",
"--create-if-missing", "-Q"]
result2 = json.loads(
bot_mode_dm.message_agent_tool(target="spark", message="ping", agent=agent)
)
assert result2["status"] == "sent"
mode, _dm_file, transport_argv = _runner_parts(calls[1]["command"])
assert mode == "stdin"
assert transport_argv == [str(hermes_entry), "-p", "default", "peer", "dm", "spark"]
def test_peer_delivery_author_carries_the_sender_hostname_and_local_stays_bare(tmp_path, monkeypatch):
"""A peer dm crosses installs, so its author id is ``bot:<hostname>/<profile>``: the peer's own ``coder`` and a
remote ``coder`` must not share one id. A teammate on this install still sees the bare ``bot:coder``."""

View File

@@ -183,7 +183,7 @@ def message_agent_tool(target: str = "", message: str = "", task_id: Optional[st
BOT_CHAT_TITLE, _handle, _hermes_root, _peers, _profile_name as _self_profile_name, _roster,
is_bot_mode_managed,
)
from tools.bot_relay import BOT_CHAT_TURN_ARGS
from tools.bot_relay import BOT_CHAT_TURN_ARGS, _hermes_cli
if _session_title(agent) != BOT_CHAT_TITLE:
return _err("message_agent is only available in a Bot Mode 'Bot Chat' session. "
@@ -231,7 +231,12 @@ def message_agent_tool(target: str = "", message: str = "", task_id: Optional[st
# Pin the registry-owning profile: `hermes peer` resolves bot_peers via the profile-scoped
# load_config(), while the roster above reads the machine-root config — the CLI must run
# in that same profile or a secondary-profile bot sees an empty registry.
return _start_delivery(["hermes", "-p", _self_profile_name(root), "peer", "dm", dm_target], content,
# The delivery runs in a background service context whose PATH lacks the gateway's
# venv bin dir, so a bare "hermes" resolves to a system install and dies on import
# under the wrong interpreter (#108628). _hermes_cli pins the entrypoint beside
# this interpreter; _delivery_lock/_local_delivery_home match argv[0] by basename,
# so the absolute path stays compatible.
return _start_delivery([_hermes_cli(), "-p", _self_profile_name(root), "peer", "dm", dm_target], content,
f"@{peer_profile or peer_name} on peer '{peer_name}'", stdin_file=True,
author=peer_author, **delivery)
@@ -252,7 +257,7 @@ def message_agent_tool(target: str = "", message: str = "", task_id: Optional[st
return _roster_err(f"No teammate named '{raw_target}' on this install, on a connected "
"machine, or on a registered peer. Pick a name from the roster "
"(roles are listed in your system prompt).")
return _start_delivery(["hermes", "-p", resolved, *BOT_CHAT_TURN_ARGS], content, f"@{_handle(resolved)}",
return _start_delivery([_hermes_cli(), "-p", resolved, *BOT_CHAT_TURN_ARGS], content, f"@{_handle(resolved)}",
stdin_file=False, profile_home=roster_homes[resolved], author=author, **delivery)