Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/stirrup/tools/code_backends/e2b.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> Comman
stderr=str(exc),
error_kind="timeout",
)
except Exception as exc:
return CommandResult(
exit_code=1,
stdout="",
stderr=str(exc),
error_kind="execution_error",
)

async def save_output_files(
self,
Expand Down
57 changes: 32 additions & 25 deletions src/stirrup/tools/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async def fetch_web_page_executor(params: FetchWebPageParams) -> ToolResult[WebF
f"{truncate_msg(body_md, MAX_LENGTH_WEB_FETCH_HTML)}</body></web_fetch>",
metadata=WebFetchMetadata(pages_fetched=[params.url]),
)
except httpx.HTTPError as exc:
except (httpx.HTTPError, ValueError) as exc:
return ToolResult(
content=f"<web_fetch><url>{params.url}</url><error>"
f"{truncate_msg(str(exc), MAX_LENGTH_WEB_FETCH_HTML)}</error></web_fetch>",
Expand Down Expand Up @@ -208,33 +208,40 @@ async def _search(query: str, http_client: httpx.AsyncClient) -> dict:

async def websearch_executor(params: WebSearchParams) -> ToolResult[WebSearchMetadata]:
"""Execute web search and format results as XML with title, URL, and description."""
# Use provided client or create temporary one for backward compatibility
if client is not None:
data = await _search(params.query, client)
else:
async with httpx.AsyncClient(timeout=WEB_SEARCH_TIMEOUT) as temp_client:
data = await _search(params.query, temp_client)

results = data.get("web", {}).get("results", [])
results_xml = (
"<results>\n"
+ "\n".join(
(
"<result>"
f"\n<title>{escape(result.get('title', '') or '')}</title>"
f"\n<url>{escape(result.get('url', '') or '')}</url>"
f"\n<description>{escape(result.get('description', '') or '')}</description>"
"\n</result>"
try:
# Use provided client or create temporary one for backward compatibility
if client is not None:
data = await _search(params.query, client)
else:
async with httpx.AsyncClient(timeout=WEB_SEARCH_TIMEOUT) as temp_client:
data = await _search(params.query, temp_client)

results = data.get("web", {}).get("results", [])
results_xml = (
"<results>\n"
+ "\n".join(
(
"<result>"
f"\n<title>{escape(result.get('title', '') or '')}</title>"
f"\n<url>{escape(result.get('url', '') or '')}</url>"
f"\n<description>{escape(result.get('description', '') or '')}</description>"
"\n</result>"
)
for result in results
)
for result in results
+ "\n</results>"
)
+ "\n</results>"
)

return ToolResult(
content=truncate_msg(results_xml, MAX_LENGTH_WEB_SEARCH_RESULTS),
metadata=WebSearchMetadata(pages_returned=len(results)),
)
return ToolResult(
content=truncate_msg(results_xml, MAX_LENGTH_WEB_SEARCH_RESULTS),
metadata=WebSearchMetadata(pages_returned=len(results)),
)
except httpx.HTTPError as exc:
return ToolResult(
content=f"<results><error>{truncate_msg(str(exc), 500)}</error></results>",
success=False,
metadata=WebSearchMetadata(pages_returned=0),
)

return Tool[WebSearchParams, WebSearchMetadata](
name="web_search",
Expand Down
7 changes: 7 additions & 0 deletions tests/test_e2b_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ async def test_run_command_exceptions(self, mock_sandbox: MagicMock) -> None:
result = await provider.run_command("sleep 1000")
assert result.error_kind == "timeout"

# Test unexpected Exception (catch-all handler)
mock_sandbox.commands.run = AsyncMock(side_effect=RuntimeError("E2B API failure"))
result = await provider.run_command("some command")
assert result.error_kind == "execution_error"
assert result.exit_code == 1
assert "E2B API failure" in result.stderr

async def test_run_command_allowlist(self, mock_sandbox: MagicMock) -> None:
"""Test command allowlist enforcement."""
provider = E2BCodeExecToolProvider(allowed_commands=[r"^echo", r"^python"])
Expand Down