mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(prompt): repair backend probe import (get_environment never existed)
The system-prompt backend probe imported a nonexistent symbol — `from tools.environments import get_environment` — which always raised ImportError: cannot import name 'get_environment'. The exception is caught and only drops the live backend description to a static fallback, so it is cosmetic, but it broke the live OS/user/cwd probe for every non-local backend (docker/singularity/modal/daytona/ssh). The real factory is `_create_environment` in tools.terminal_tool. Build the environment the same way the live terminal path does (select backend image, assemble ssh/container config from _get_env_config()), then run the probe. Note: this does NOT affect tool loading — tool selection runs each tool's check_fn and never consults this probe. Regression from #52147 (2026-06-25). Closes #53667 (probe import); the 'cronjob-only' tool-collapse symptom is not reproducible — tool selection has no probe dependency and memory's check_fn is unconditionally True.
This commit is contained in:
parent
b508d4296e
commit
aa50c1ba5d
2 changed files with 94 additions and 3 deletions
|
|
@ -926,8 +926,7 @@ def _probe_remote_backend(env_type: str) -> str | None:
|
|||
try:
|
||||
# Import locally: tools/ imports are heavy and only relevant when a
|
||||
# non-local backend is actually configured.
|
||||
from tools.terminal_tool import _get_env_config # type: ignore
|
||||
from tools.environments import get_environment # type: ignore
|
||||
from tools.terminal_tool import _create_environment, _get_env_config # type: ignore
|
||||
except Exception as e:
|
||||
logger.debug("Backend probe unavailable (import failed): %s", e)
|
||||
_BACKEND_PROBE_CACHE[cache_key] = ""
|
||||
|
|
@ -935,7 +934,59 @@ def _probe_remote_backend(env_type: str) -> str | None:
|
|||
|
||||
try:
|
||||
config = _get_env_config()
|
||||
env = get_environment(config)
|
||||
# Build the environment the same way tools/terminal_tool.py does for a
|
||||
# live command: select the backend image, then assemble ssh/container
|
||||
# config from the env-derived dict. (There is no `get_environment`
|
||||
# factory — the real entry point is `_create_environment`.)
|
||||
if env_type == "docker":
|
||||
image = config.get("docker_image", "")
|
||||
elif env_type == "singularity":
|
||||
image = config.get("singularity_image", "")
|
||||
elif env_type == "modal":
|
||||
image = config.get("modal_image", "")
|
||||
elif env_type == "daytona":
|
||||
image = config.get("daytona_image", "")
|
||||
else:
|
||||
image = ""
|
||||
|
||||
ssh_config = None
|
||||
if env_type == "ssh":
|
||||
ssh_config = {
|
||||
"host": config.get("ssh_host", ""),
|
||||
"user": config.get("ssh_user", ""),
|
||||
"port": config.get("ssh_port", 22),
|
||||
"key": config.get("ssh_key", ""),
|
||||
"persistent": config.get("ssh_persistent", False),
|
||||
}
|
||||
|
||||
container_config = None
|
||||
if env_type in {"docker", "singularity", "modal", "daytona"}:
|
||||
container_config = {
|
||||
"container_cpu": config.get("container_cpu", 1),
|
||||
"container_memory": config.get("container_memory", 5120),
|
||||
"container_disk": config.get("container_disk", 51200),
|
||||
"container_persistent": config.get("container_persistent", True),
|
||||
"modal_mode": config.get("modal_mode", "auto"),
|
||||
"docker_volumes": config.get("docker_volumes", []),
|
||||
"docker_mount_cwd_to_workspace": config.get("docker_mount_cwd_to_workspace", False),
|
||||
"docker_forward_env": config.get("docker_forward_env", []),
|
||||
"docker_env": config.get("docker_env", {}),
|
||||
"docker_run_as_host_user": config.get("docker_run_as_host_user", False),
|
||||
"docker_extra_args": config.get("docker_extra_args", []),
|
||||
"docker_persist_across_processes": config.get("docker_persist_across_processes", True),
|
||||
"docker_orphan_reaper": config.get("docker_orphan_reaper", True),
|
||||
}
|
||||
|
||||
env = _create_environment(
|
||||
env_type=env_type,
|
||||
image=image,
|
||||
cwd=config.get("cwd", ""),
|
||||
timeout=config.get("timeout", 180),
|
||||
ssh_config=ssh_config,
|
||||
container_config=container_config,
|
||||
task_id="prompt-backend-probe",
|
||||
host_cwd=config.get("host_cwd"),
|
||||
)
|
||||
# Single-line POSIX probe — works on any Unixy backend. Wrapped in
|
||||
# `2>/dev/null` so a missing binary doesn't pollute the output.
|
||||
probe_cmd = (
|
||||
|
|
|
|||
|
|
@ -1219,6 +1219,46 @@ class TestEnvironmentHints:
|
|||
assert "Linux 6.8.0" in result
|
||||
assert "/workspace" in result
|
||||
|
||||
def test_probe_remote_backend_imports_real_factory(self, monkeypatch):
|
||||
"""Regression for #53667: the probe imported a nonexistent
|
||||
``get_environment`` from ``tools.environments`` and always died with
|
||||
``ImportError: cannot import name 'get_environment'`` (cosmetic — it
|
||||
only dropped the live backend description to a static fallback). The
|
||||
real factory is ``_create_environment`` in ``tools.terminal_tool``;
|
||||
the probe must import and call THAT, returning a parsed line instead
|
||||
of None."""
|
||||
import agent.prompt_builder as _pb
|
||||
|
||||
monkeypatch.setenv("TERMINAL_ENV", "docker")
|
||||
_pb._clear_backend_probe_cache()
|
||||
|
||||
class _FakeEnv:
|
||||
def execute(self, cmd, timeout=None):
|
||||
return {
|
||||
"returncode": 0,
|
||||
"output": (
|
||||
"os=Linux\nkernel=6.8.0\nhome=/root\n"
|
||||
"cwd=/workspace\nuser=root\n"
|
||||
),
|
||||
}
|
||||
|
||||
created = {}
|
||||
|
||||
def _fake_create_environment(*, env_type, **kwargs):
|
||||
created["env_type"] = env_type
|
||||
return _FakeEnv()
|
||||
|
||||
# Patch the REAL factory in tools.terminal_tool — the probe imports it
|
||||
# locally, so the import itself must succeed (the bug was here).
|
||||
import tools.terminal_tool as _tt
|
||||
monkeypatch.setattr(_tt, "_create_environment", _fake_create_environment)
|
||||
|
||||
line = _pb._probe_remote_backend("docker")
|
||||
assert created.get("env_type") == "docker"
|
||||
assert line is not None
|
||||
assert "Linux 6.8.0" in line
|
||||
assert "root" in line
|
||||
|
||||
def test_remote_backend_list_covers_known_sandboxes(self):
|
||||
"""Regression guard: if someone adds a remote backend, they must list it here."""
|
||||
import agent.prompt_builder as _pb
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue