From 093acd72dd2e2391cbb9f34ce940ca3e58b1fb9a Mon Sep 17 00:00:00 2001 From: teknium1 Date: Wed, 4 Mar 2026 14:22:30 -0800 Subject: [PATCH] fix: catch exceptions from check_fn in is_toolset_available() get_definitions() already wrapped check_fn() calls in try/except, but is_toolset_available() did not. A failing check (network error, missing import, bad config) would propagate uncaught and crash the CLI banner, agent startup, and tools-info display. Now is_toolset_available() catches all exceptions and returns False, matching the existing pattern in get_definitions(). Added 4 tests covering exception handling in is_toolset_available(), check_toolset_requirements(), get_definitions(), and check_tool_availability(). Closes #402 --- tests/tools/test_registry.py | 54 ++++++++++++++++++++++++++++++++++++ tools/registry.py | 14 ++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_registry.py b/tests/tools/test_registry.py index 58b1c6327..07ebffe11 100644 --- a/tests/tools/test_registry.py +++ b/tests/tools/test_registry.py @@ -119,3 +119,57 @@ class TestToolsetAvailability: result = json.loads(reg.dispatch("bad", {})) assert "error" in result assert "RuntimeError" in result["error"] + + +class TestCheckFnExceptionHandling: + """Verify that a raising check_fn is caught rather than crashing.""" + + def test_is_toolset_available_catches_exception(self): + reg = ToolRegistry() + reg.register( + name="t", + toolset="broken", + schema=_make_schema(), + handler=_dummy_handler, + check_fn=lambda: 1 / 0, # ZeroDivisionError + ) + # Should return False, not raise + assert reg.is_toolset_available("broken") is False + + def test_check_toolset_requirements_survives_raising_check(self): + reg = ToolRegistry() + reg.register(name="a", toolset="good", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: True) + reg.register(name="b", toolset="bad", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: (_ for _ in ()).throw(ImportError("no module"))) + + reqs = reg.check_toolset_requirements() + assert reqs["good"] is True + assert reqs["bad"] is False + + def test_get_definitions_skips_raising_check(self): + reg = ToolRegistry() + reg.register( + name="ok_tool", + toolset="s", + schema=_make_schema("ok_tool"), + handler=_dummy_handler, + check_fn=lambda: True, + ) + reg.register( + name="bad_tool", + toolset="s2", + schema=_make_schema("bad_tool"), + handler=_dummy_handler, + check_fn=lambda: (_ for _ in ()).throw(OSError("network down")), + ) + defs = reg.get_definitions({"ok_tool", "bad_tool"}) + assert len(defs) == 1 + assert defs[0]["function"]["name"] == "ok_tool" + + def test_check_tool_availability_survives_raising_check(self): + reg = ToolRegistry() + reg.register(name="a", toolset="works", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: True) + reg.register(name="b", toolset="crashes", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: 1 / 0) + + available, unavailable = reg.check_tool_availability() + assert "works" in available + assert any(u["name"] == "crashes" for u in unavailable) diff --git a/tools/registry.py b/tools/registry.py index 5605f319e..fccfbd238 100644 --- a/tools/registry.py +++ b/tools/registry.py @@ -146,9 +146,19 @@ class ToolRegistry: return {name: e.toolset for name, e in self._tools.items()} def is_toolset_available(self, toolset: str) -> bool: - """Check if a toolset's requirements are met.""" + """Check if a toolset's requirements are met. + + Returns False (rather than crashing) when the check function raises + an unexpected exception (e.g. network error, missing import, bad config). + """ check = self._toolset_checks.get(toolset) - return check() if check else True + if not check: + return True + try: + return bool(check()) + except Exception: + logger.debug("Toolset %s check raised; marking unavailable", toolset) + return False def check_toolset_requirements(self) -> Dict[str, bool]: """Return ``{toolset: available_bool}`` for every toolset."""