fix(agent): prevent safety guard false positives and streamed message drop

Three independent fixes for issues exposed by PR #3493:

1. shell.py: allow /dev/* paths in workspace guard
   Commands like `rm file.txt 2>/dev/null` were blocked because
   _extract_absolute_paths captured /dev/null as a path outside
   the workspace. Allow /dev like media_path is already allowed.

2. shell.py: remove | from home_paths regex prefix
   Loki query operator `|~` was misinterpreted as pipe + home
   directory, causing false workspace violation errors.

3. loop.py: change _streamed from blacklist to whitelist
   stop_reason "tool_error" was not in the exclusion set
   {"ask_user", "error"}, so _streamed=True was set on fatal
   errors. channel manager then skipped channel.send() because
   it assumed the content was already streamed — but it never
   was. Whitelist to only {"stop", "end_turn", "max_tokens"}.

Also fixes a pre-existing Windows bug in _spawn where
create_subprocess_exec + list2cmdline breaks commands with
paths containing spaces (e.g. D:\Program Files\python.exe).

Closes: #3599, #3605
This commit is contained in:
chengyongru
2026-05-04 01:25:52 +08:00
committed by Xubin Ren
parent 2a7433b7ec
commit d3689d143c
4 changed files with 45 additions and 20 deletions
+13 -15
View File
@@ -112,33 +112,31 @@ class TestSpawnUnix:
class TestSpawnWindows:
@pytest.mark.asyncio
async def test_uses_comspec_from_env(self):
async def test_uses_create_subprocess_shell(self):
env = {"COMSPEC": r"C:\Windows\system32\cmd.exe", "PATH": ""}
with (
patch("nanobot.agent.tools.shell._IS_WINDOWS", True),
patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec,
patch("asyncio.create_subprocess_shell", new_callable=AsyncMock) as mock_shell,
):
mock_exec.return_value = AsyncMock()
await ExecTool._spawn("dir", r"C:\Users", env)
mock_shell.return_value = AsyncMock()
await ExecTool._spawn("dir", r"C:\work", env)
args = mock_exec.call_args[0]
assert "cmd.exe" in args[0]
assert "/c" in args
args = mock_shell.call_args[0]
assert "dir" in args
@pytest.mark.asyncio
async def test_falls_back_to_default_comspec(self):
env = {"PATH": ""}
async def test_passes_cwd_and_env(self):
env = {"PATH": "/usr/bin"}
with (
patch("nanobot.agent.tools.shell._IS_WINDOWS", True),
patch.dict("os.environ", {}, clear=True),
patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec,
patch("asyncio.create_subprocess_shell", new_callable=AsyncMock) as mock_shell,
):
mock_exec.return_value = AsyncMock()
await ExecTool._spawn("dir", r"C:\Users", env)
mock_shell.return_value = AsyncMock()
await ExecTool._spawn("echo hi", r"C:\work", env)
args = mock_exec.call_args[0]
assert args[0] == "cmd.exe"
kwargs = mock_shell.call_args[1]
assert kwargs["cwd"] == r"C:\work"
assert kwargs["env"] == env
# ---------------------------------------------------------------------------