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:
@@ -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``."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user