From aa50c1ba5d277c97f78f27f38f0075c09ce9298c Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:16:28 -0700 Subject: [PATCH] fix(prompt): repair backend probe import (get_environment never existed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- agent/prompt_builder.py | 57 ++++++++++++++++++++++++++++-- tests/agent/test_prompt_builder.py | 40 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index e136a9476a6..319be7255e2 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -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 = ( diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index a77c24f34a7..a2d8ec56d7e 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -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