fix(tools): resolve the sandbox interpreter for workspace file scripts

The workspace file tools (read/write/edit/glob/grep) ran a script through
`python`, which a sandbox built from a host rootfs may not provide: those hosts
ship `python3` only, and a read-only /usr prevents creating a `python` shim
inside the jail. Every file tool then failed with `/bin/sh: 1: python: not
found`. Resolve the interpreter with `command -v python3 || command -v python`,
matching the interpreter the Box service already uses for its own scripts.
This commit is contained in:
Hyu
2026-09-30 18:44:16 +08:00
parent 0c3917ee0f
commit ff3f321120
2 changed files with 40 additions and 1 deletions
@@ -754,9 +754,16 @@ 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 = (
'PYTHON_BIN=$(command -v python3 || command -v python); '
f"\"$PYTHON_BIN\" - <<'PY'\n{script}\nPY"
)
result = await self.ap.box_service.execute_tool(
{
'command': f"python - <<'PY'\n{script}\nPY",
'command': command,
'timeout_sec': 30,
},
query,
@@ -851,3 +851,35 @@ async def test_box_grep_script_serializes_optional_include_as_python(monkeypatch
values[statement.targets[0].id] = ast.literal_eval(statement.value)
assert values['include'] == include
assert values['path'] == '/workspace'
@pytest.mark.asyncio
async def test_workspace_file_script_resolves_the_sandbox_interpreter():
"""Sandboxes built from a host rootfs may only ship `python3`."""
captured: dict = {}
async def execute_tool(payload, query):
captured.update(payload)
return {'ok': True, 'stdout': '{"ok": true}'}
box_service = SimpleNamespace(
available=True,
execute_tool=execute_tool,
require_workspace_sandbox=AsyncMock(),
)
loader = NativeToolLoader(SimpleNamespace(box_service=box_service, logger=Mock()))
query = SimpleNamespace(
_execution_context=_CONTEXT,
bot_uuid=None,
pipeline_uuid=None,
query_uuid=None,
)
query._box_binding = RunBoxBinding('run', 'box', {}, 'run')
result = await loader._run_workspace_file_script('print("x")', query)
assert result == {'ok': True}
command = captured['command']
assert command.startswith('PYTHON_BIN=$(command -v python3 || command -v python)')
assert command.endswith(" - <<'PY'\nprint(\"x\")\nPY")