diff --git a/secator/ai/actions.py b/secator/ai/actions.py index c2c1def39..1366529b7 100644 --- a/secator/ai/actions.py +++ b/secator/ai/actions.py @@ -811,7 +811,14 @@ def _handle_query(action: Dict, ctx: ActionContext) -> Generator: """ context = _get_result_context(action, ctx) query_filter = action.get("query", {}) + # The schema declares `limit` an integer, but some models send it as a string + # ("10"); a str limit reaches the backend and raises `'>=' not supported between + # int and str`. Coerce to int (bad/None values fall back to the default). limit = action.get("limit", 100) + try: + limit = int(limit) + except (TypeError, ValueError): + limit = 100 # The query_workspace tool schema declares `query` as an object, but some # models/providers serialize it as a JSON *string* (a known tool-calling diff --git a/tests/unit/test_ai_actions.py b/tests/unit/test_ai_actions.py index ca5fac58a..7e80ea6a4 100644 --- a/tests/unit/test_ai_actions.py +++ b/tests/unit/test_ai_actions.py @@ -424,6 +424,27 @@ def test_query_local_driver_exempt_from_workspace_guard(self): uuids = {r.get('_uuid') for r in results if isinstance(r, dict)} self.assertIn('live1', uuids) + def test_query_stringified_limit_coerced_to_int(self): + """A model-supplied string limit ('10') is coerced to int before the backend + (a str limit raises TypeError: '>=' not supported between int and str).""" + mock_engine = MagicMock() + mock_engine.backend.name = "mongodb" + mock_engine.search.return_value = [] + ctx = ActionContext(targets=['t'], model='m', context={'workspace_id': 'ws1'}) + with patch.object(ctx, 'get_query_engine', return_value=mock_engine): + list(_handle_query({'action': 'query', 'query': {'_type': 'ip'}, 'limit': '10'}, ctx)) + self.assertEqual(mock_engine.search.call_args.kwargs.get('limit'), 10) + + def test_query_bad_limit_falls_back_to_default(self): + """A non-numeric limit falls back to the default (100), not a crash.""" + mock_engine = MagicMock() + mock_engine.backend.name = "mongodb" + mock_engine.search.return_value = [] + ctx = ActionContext(targets=['t'], model='m', context={'workspace_id': 'ws1'}) + with patch.object(ctx, 'get_query_engine', return_value=mock_engine): + list(_handle_query({'action': 'query', 'query': {}, 'limit': 'notanumber'}, ctx)) + self.assertEqual(mock_engine.search.call_args.kwargs.get('limit'), 100) + @unittest.skipUnless(ADDONS_ENABLED['ai'], 'ai addon not installed') class TestRunRunner(unittest.TestCase):