fix(bot-mode): keep delivery runner on host backend
This commit is contained in:
@@ -238,6 +238,8 @@ def test_local_delivery_command_and_ack(tmp_path, monkeypatch):
|
||||
call = calls[0]
|
||||
assert call["background"] is True
|
||||
assert call["notify_on_complete"] is True
|
||||
assert call["_host_local"] is True
|
||||
assert Path(call["workdir"]) == Path(bot_mode_dm.__file__).resolve().parent.parent
|
||||
command = call["command"]
|
||||
mode, dm_file, transport_argv = _runner_parts(command)
|
||||
assert mode == "query-file"
|
||||
@@ -469,6 +471,38 @@ def test_real_delivery_command_round_trip(tmp_path, stdin_file):
|
||||
assert not dm_file.exists()
|
||||
|
||||
|
||||
@pytest.mark.windows_only
|
||||
def test_delivery_command_round_trip_through_windows_local_shell(tmp_path):
|
||||
"""Native runner paths must survive the Git Bash process boundary."""
|
||||
from tools.environments.local import _find_shell
|
||||
|
||||
dm_file = tmp_path / "message with spaces.txt"
|
||||
dm_file.write_text("secret", encoding="utf-8")
|
||||
observed = tmp_path / "observed with spaces.txt"
|
||||
child = tmp_path / "child with spaces.py"
|
||||
child.write_text(
|
||||
"import pathlib, sys\n"
|
||||
"pathlib.Path(sys.argv[1]).write_text('started', encoding='utf-8')\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
command = bot_mode_dm._delivery_command(
|
||||
[sys.executable, str(child), str(observed)],
|
||||
str(dm_file),
|
||||
stdin_file=False,
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[_find_shell(), "-lic", command],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
assert observed.read_text(encoding="utf-8") == "started"
|
||||
assert not dm_file.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("terminal_result", "raises"),
|
||||
[
|
||||
|
||||
@@ -173,6 +173,73 @@ def test_background_command_prefers_recorded_session_cwd_over_init_time_cwd(monk
|
||||
}]
|
||||
|
||||
|
||||
def test_host_local_background_command_bypasses_configured_backend(tmp_path, monkeypatch):
|
||||
"""Hermes control-plane children stay on the host when tools use Docker."""
|
||||
calls = []
|
||||
|
||||
class FakeEnv:
|
||||
env = {}
|
||||
cwd = str(tmp_path)
|
||||
|
||||
class FakeRegistry:
|
||||
pending_watchers = []
|
||||
|
||||
def spawn_local(self, **kwargs):
|
||||
calls.append(("local", kwargs))
|
||||
return SimpleNamespace(id="proc_host", pid=1234)
|
||||
|
||||
def spawn_via_env(self, **kwargs):
|
||||
calls.append(("configured", kwargs))
|
||||
raise AssertionError("host-local command reached configured backend")
|
||||
|
||||
import tools.process_registry as process_registry_mod
|
||||
import tools.self_repo_guard as self_repo_guard
|
||||
|
||||
task_id = "bot-delivery"
|
||||
monkeypatch.setattr(
|
||||
terminal_tool,
|
||||
"_get_env_config",
|
||||
lambda: {
|
||||
"env_type": "docker",
|
||||
"docker_image": "python:3.11",
|
||||
"cwd": str(tmp_path),
|
||||
"timeout": 60,
|
||||
"lifetime_seconds": 3600,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
terminal_tool,
|
||||
"_active_environments",
|
||||
{f"host-local-{task_id}": FakeEnv()},
|
||||
)
|
||||
monkeypatch.setattr(terminal_tool, "_last_activity", {})
|
||||
monkeypatch.setattr(terminal_tool, "_task_env_overrides", {})
|
||||
monkeypatch.setattr(terminal_tool, "_start_cleanup_thread", lambda: None)
|
||||
monkeypatch.setattr(terminal_tool, "_resolve_container_task_id", lambda value: value)
|
||||
monkeypatch.setattr(terminal_tool, "_docker_has_host_access", lambda config: False)
|
||||
monkeypatch.setattr(
|
||||
terminal_tool,
|
||||
"_check_all_guards",
|
||||
lambda command, env_type, **kwargs: {"approved": env_type == "local"},
|
||||
)
|
||||
monkeypatch.setattr(self_repo_guard, "guard_active", lambda: False)
|
||||
monkeypatch.setattr(process_registry_mod, "process_registry", FakeRegistry())
|
||||
|
||||
result = json.loads(
|
||||
terminal_tool.terminal_tool(
|
||||
command="host-runner",
|
||||
task_id=task_id,
|
||||
workdir=str(tmp_path),
|
||||
background=True,
|
||||
_host_local=True,
|
||||
)
|
||||
)
|
||||
|
||||
assert result["session_id"] == "proc_host"
|
||||
assert calls[0][0] == "local"
|
||||
assert calls[0][1]["task_id"] == f"host-local-{task_id}"
|
||||
|
||||
|
||||
def test_safe_getcwd_falls_back_to_home_when_no_terminal_cwd(monkeypatch):
|
||||
def _boom():
|
||||
raise FileNotFoundError()
|
||||
|
||||
@@ -613,6 +613,12 @@ def _delivery_command(argv: list[str], dm_file: str, *, stdin_file: bool) -> str
|
||||
dm_file,
|
||||
*argv,
|
||||
]
|
||||
if sys.platform == "win32":
|
||||
# The tracked local backend uses Git Bash on native Windows. Forward
|
||||
# slashes preserve native drive paths while remaining executable by
|
||||
# that shell; backslash-form paths are parsed as command names and die
|
||||
# with exit 127 before this runner starts.
|
||||
runner_argv = [part.replace("\\", "/") for part in runner_argv]
|
||||
return shlex.join(runner_argv)
|
||||
|
||||
|
||||
@@ -664,6 +670,8 @@ def _spawn_delivery(
|
||||
background=True,
|
||||
notify_on_complete=True,
|
||||
task_id=task_id,
|
||||
workdir=str(Path(__file__).resolve().parent.parent),
|
||||
_host_local=True,
|
||||
)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
|
||||
@@ -2827,6 +2827,7 @@ def terminal_tool(
|
||||
pty: bool = False,
|
||||
notify_on_complete: bool = False,
|
||||
watch_patterns: Optional[List[str]] = None,
|
||||
_host_local: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Execute a command in the configured terminal environment.
|
||||
@@ -2874,13 +2875,18 @@ def terminal_tool(
|
||||
|
||||
# Get configuration
|
||||
config = _get_env_config()
|
||||
env_type = config["env_type"]
|
||||
env_type = "local" if _host_local else config["env_type"]
|
||||
|
||||
# Use task_id for environment isolation. By default all subagent
|
||||
# task_ids collapse back to "default" so the top-level agent and
|
||||
# every delegate_task child share one container; only task_ids with
|
||||
# a registered env override (RL benchmarks) get isolated sandboxes.
|
||||
effective_task_id = _resolve_container_task_id(task_id)
|
||||
if _host_local:
|
||||
# Hermes-owned control-plane children must run beside the current
|
||||
# interpreter, never inside the model's configured Docker/SSH/etc.
|
||||
# Keep their environment cache separate from the configured backend.
|
||||
effective_task_id = f"host-local-{effective_task_id}"
|
||||
|
||||
# Check per-task overrides (set by environments like TerminalBench2Env)
|
||||
# before falling back to global env var config. ``resolve_task_overrides``
|
||||
|
||||
Reference in New Issue
Block a user