fix(bot-mode): keep delivery runner on host backend

This commit is contained in:
fangliquanflq
2026-08-26 22:11:26 +08:00
committed by Teknium
parent 253b9d78c1
commit dd401e0f15
4 changed files with 116 additions and 1 deletions

View File

@@ -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"),
[

View File

@@ -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()

View File

@@ -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)

View File

@@ -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``