fix(backend/copilot): return sandbox path from bridge, inform model of copy location

Address CodeRabbit review: _bridge_to_sandbox now returns the sandbox
path (or None on failure) so callers can append "[Sandbox copy available
at /tmp/file.json]" to the Read result. This gives the model explicit
feedback about where to find the file in the sandbox, instead of
silently bridging with no indication.
This commit is contained in:
Zamil Majdy
2026-04-02 08:03:36 +02:00
parent 263cd0ecac
commit b5b754d5eb
3 changed files with 30 additions and 12 deletions

View File

@@ -141,7 +141,11 @@ async def _handle_read_file(args: dict[str, Any]) -> dict[str, Any]:
if not result.get("isError"):
sandbox = _get_sandbox()
if sandbox is not None:
await _bridge_to_sandbox(sandbox, file_path, offset, limit)
bridged = await _bridge_to_sandbox(sandbox, file_path, offset, limit)
if bridged:
result["content"][0][
"text"
] += f"\n[Sandbox copy available at {bridged}]"
return result
result = _get_sandbox_and_path(file_path)
@@ -321,7 +325,7 @@ _BRIDGE_SKIP_BYTES = 50 * 1024 * 1024 # 50 MB
async def _bridge_to_sandbox(
sandbox: Any, file_path: str, offset: int, limit: int
) -> None:
) -> str | None:
"""Best-effort copy of a host-side SDK file into the E2B sandbox.
When the model reads an SDK-internal file (e.g. tool-results), it often
@@ -331,6 +335,8 @@ async def _bridge_to_sandbox(
Only copies when offset=0 and limit is large enough to indicate the model
wants the full file. Errors are logged but never propagated.
Returns the sandbox path on success, or ``None`` on skip/failure.
Size handling:
- <= 5 MB: written to ``/tmp/<basename>`` via shell base64 (``_sandbox_write``).
- 5-50 MB: written to ``/home/user/<basename>`` via ``sandbox.files.write()``
@@ -338,7 +344,7 @@ async def _bridge_to_sandbox(
- > 50 MB: skipped entirely with a warning.
"""
if offset != 0 or limit < 2000:
return
return None
basename = os.path.basename(file_path)
try:
expanded = os.path.realpath(os.path.expanduser(file_path))
@@ -365,12 +371,14 @@ async def _bridge_to_sandbox(
logger.info(
"[E2B] Bridged SDK file to sandbox: %s -> %s", basename, sandbox_path
)
return sandbox_path
except Exception:
logger.debug(
"[E2B] Failed to bridge SDK file to sandbox: %s",
basename,
exc_info=True,
)
return None
# Local read (for SDK-internal paths)

View File

@@ -380,8 +380,9 @@ class TestBridgeToSandbox:
f.write_text('{"ok": true}')
sandbox = _make_bridge_sandbox()
await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
result = await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
assert result == "/tmp/result.json"
sandbox.commands.run.assert_called_once()
cmd = sandbox.commands.run.call_args[0][0]
assert "result.json" in cmd
@@ -394,8 +395,9 @@ class TestBridgeToSandbox:
f.write_text("content")
sandbox = _make_bridge_sandbox()
await _bridge_to_sandbox(sandbox, str(f), offset=10, limit=2000)
result = await _bridge_to_sandbox(sandbox, str(f), offset=10, limit=2000)
assert result is None
sandbox.commands.run.assert_not_called()
sandbox.files.write.assert_not_called()
@@ -424,14 +426,16 @@ class TestBridgeToSandbox:
sandbox.files.write.assert_not_called()
@pytest.mark.asyncio
async def test_sandbox_write_failure_does_not_raise(self, tmp_path):
"""If sandbox write fails, the error is swallowed (best-effort)."""
async def test_sandbox_write_failure_returns_none(self, tmp_path):
"""If sandbox write fails, returns None (best-effort)."""
f = tmp_path / "data.txt"
f.write_text("content")
sandbox = _make_bridge_sandbox()
sandbox.commands.run.side_effect = RuntimeError("E2B timeout")
await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
result = await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
assert result is None
@pytest.mark.asyncio
async def test_large_file_uses_files_api(self, tmp_path):
@@ -440,8 +444,9 @@ class TestBridgeToSandbox:
f.write_bytes(b"x" * (_BRIDGE_SHELL_MAX_BYTES + 1))
sandbox = _make_bridge_sandbox()
await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
result = await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
assert result == "/home/user/big.json"
sandbox.files.write.assert_called_once()
call_args = sandbox.files.write.call_args[0]
assert call_args[0] == "/home/user/big.json"
@@ -457,7 +462,9 @@ class TestBridgeToSandbox:
fh.write(b"\0")
sandbox = _make_bridge_sandbox()
await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
result = await _bridge_to_sandbox(sandbox, str(f), offset=0, limit=2000)
assert result is None
sandbox.commands.run.assert_not_called()
sandbox.files.write.assert_not_called()

View File

@@ -390,10 +390,13 @@ async def _read_file_handler(args: dict[str, Any]) -> dict[str, Any]:
#
# When E2B is active, also copy the file into the sandbox so
# bash_exec can process it (the model often uses Read then bash).
text = "".join(selected)
sandbox = _current_sandbox.get(None)
if sandbox is not None:
await _bridge_to_sandbox(sandbox, file_path, offset, limit)
return _mcp_ok("".join(selected))
bridged = await _bridge_to_sandbox(sandbox, file_path, offset, limit)
if bridged:
text += f"\n[Sandbox copy available at {bridged}]"
return _mcp_ok(text)
except FileNotFoundError:
return _mcp_err(f"File not found: {file_path}")
except Exception as e: