diff --git a/src/stirrup/tools/code_backends/e2b.py b/src/stirrup/tools/code_backends/e2b.py index eeaf82c..d489b92 100644 --- a/src/stirrup/tools/code_backends/e2b.py +++ b/src/stirrup/tools/code_backends/e2b.py @@ -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, diff --git a/src/stirrup/tools/web.py b/src/stirrup/tools/web.py index 1da23e6..d0759b5 100644 --- a/src/stirrup/tools/web.py +++ b/src/stirrup/tools/web.py @@ -121,7 +121,7 @@ async def fetch_web_page_executor(params: FetchWebPageParams) -> ToolResult[WebF f"{truncate_msg(body_md, MAX_LENGTH_WEB_FETCH_HTML)}", metadata=WebFetchMetadata(pages_fetched=[params.url]), ) - except httpx.HTTPError as exc: + except (httpx.HTTPError, ValueError) as exc: return ToolResult( content=f"{params.url}" f"{truncate_msg(str(exc), MAX_LENGTH_WEB_FETCH_HTML)}", @@ -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 = ( - "\n" - + "\n".join( - ( - "" - f"\n{escape(result.get('title', '') or '')}" - f"\n{escape(result.get('url', '') or '')}" - f"\n{escape(result.get('description', '') or '')}" - "\n" + 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 = ( + "\n" + + "\n".join( + ( + "" + f"\n{escape(result.get('title', '') or '')}" + f"\n{escape(result.get('url', '') or '')}" + f"\n{escape(result.get('description', '') or '')}" + "\n" + ) + for result in results ) - for result in results + + "\n" ) - + "\n" - ) - 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"{truncate_msg(str(exc), 500)}", + success=False, + metadata=WebSearchMetadata(pages_returned=0), + ) return Tool[WebSearchParams, WebSearchMetadata]( name="web_search", diff --git a/tests/test_e2b_execution.py b/tests/test_e2b_execution.py index b930175..eacefba 100644 --- a/tests/test_e2b_execution.py +++ b/tests/test_e2b_execution.py @@ -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"])