From bce94bd0d9472ff93df99b50c2cf7747f1e149c2 Mon Sep 17 00:00:00 2001 From: Li-john1021 Date: Mon, 19 Oct 2026 14:46:47 +0800 Subject: [PATCH] fix(tools): require python3 for Box native file scripts (#2658) * fix(tools): require python3 for Box native file scripts Unversioned `python` is still Python 2 on some host-rootfs nsjail mounts. Falling back to it made read/write/edit scripts fail with SyntaxError on f-strings (#2603). Resolve only `python3`, and return a clear JSON error when it is missing instead of invoking Python 2. Fixes #2603 * style(tools): format Box interpreter error command Co-authored-by: multica-agent --------- Co-authored-by: Li-john1021 Co-authored-by: Chin Rock Co-authored-by: multica-agent --- src/langbot/pkg/provider/tools/loaders/native.py | 16 ++++++++++++---- .../provider/test_tool_manager_native.py | 6 ++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/langbot/pkg/provider/tools/loaders/native.py b/src/langbot/pkg/provider/tools/loaders/native.py index 9d03deb26..5182ee9af 100644 --- a/src/langbot/pkg/provider/tools/loaders/native.py +++ b/src/langbot/pkg/provider/tools/loaders/native.py @@ -747,10 +747,18 @@ class NativeToolLoader(loader.ToolLoader): return f'{str(base).rstrip("/")}/{relative}' async def _run_workspace_file_script(self, script: str, query: pipeline_query.Query) -> dict: - # Sandbox images built from a host rootfs may only ship `python3`, and a - # read-only /usr prevents creating a `python` shim inside the jail, so - # resolve the interpreter instead of assuming its name. - command = f'PYTHON_BIN=$(command -v python3 || command -v python); "$PYTHON_BIN" - <<\'PY\'\n{script}\nPY' + # Box file tools need Python 3 (f-strings, etc.). Prefer `python3` and + # never fall back to unversioned `python`, which may still be Python 2 + # on host-rootfs nsjail mounts (LangBot #2603). Missing python3 yields a + # clear JSON error instead of a SyntaxError tool result. + command = ( + 'PYTHON_BIN=$(command -v python3 || true); ' + 'if [ -z "$PYTHON_BIN" ]; then ' + 'printf \'%s\\n\' \'{"ok": false, "error": "Python 3 interpreter not found in Box (python3 required for native file tools)"}\'; ' + 'exit 0; ' + 'fi; ' + f'"$PYTHON_BIN" - <<\'PY\'\n{script}\nPY' + ) result = await self.ap.box_service.execute_tool( { 'command': command, diff --git a/tests/unit_tests/provider/test_tool_manager_native.py b/tests/unit_tests/provider/test_tool_manager_native.py index 767e1d7b6..7dac7691c 100644 --- a/tests/unit_tests/provider/test_tool_manager_native.py +++ b/tests/unit_tests/provider/test_tool_manager_native.py @@ -866,7 +866,7 @@ async def test_box_grep_script_serializes_optional_include_as_python(monkeypatch @pytest.mark.asyncio async def test_workspace_file_script_resolves_the_sandbox_interpreter(): - """Sandboxes built from a host rootfs may only ship `python3`.""" + """Native file tools must use python3 and never fall back to python2.""" captured: dict = {} @@ -892,5 +892,7 @@ async def test_workspace_file_script_resolves_the_sandbox_interpreter(): assert result == {'ok': True} command = captured['command'] - assert command.startswith('PYTHON_BIN=$(command -v python3 || command -v python)') + assert 'command -v python3 || true' in command + assert 'python3 required for native file tools' in command assert command.endswith(' - <<\'PY\'\nprint("x")\nPY') + assert '|| command -v python)' not in command