diff --git a/packages/toolbox-core/src/toolbox_core/client.py b/packages/toolbox-core/src/toolbox_core/client.py index b16138afb..7b39c270a 100644 --- a/packages/toolbox-core/src/toolbox_core/client.py +++ b/packages/toolbox-core/src/toolbox_core/client.py @@ -485,6 +485,11 @@ async def load_toolset( ) tools.append(tool) + # Track usage for both modes. Skipping this under strict=True left + # overall-used sets empty and made the final toolset check always fail. + overall_used_auth_keys.update(used_auth_keys) + overall_used_bound_params.update(used_bound_keys) + if strict: validate_unused_requirements( provided_auth_keys, @@ -494,9 +499,6 @@ async def load_toolset( tool_name, is_toolset=False, ) - else: - overall_used_auth_keys.update(used_auth_keys) - overall_used_bound_params.update(used_bound_keys) validate_unused_requirements( provided_auth_keys, diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index 405558c9a..6567e4743 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -515,6 +515,28 @@ async def test_load_tool_with_unused_bound_param_fail( TOOL_NAME, bound_params={"unused_param": "some_value"} ) + @pytest.mark.asyncio + async def test_load_toolset_strict_with_fully_used_bound_param_success( + self, mock_transport, tool_schema_with_param_P + ): + """Tests that load_toolset succeeds in strict mode when every tool uses all bound params.""" + TOOL_P = "tool_with_p" + manifest = ManifestSchema( + serverVersion="0.0.0", + tools={TOOL_P: tool_schema_with_param_P}, + ) + mock_transport.tools_list_mock.return_value = manifest + + async with ToolboxClient(TEST_BASE_URL) as client: + client._ToolboxClient__transport = mock_transport + tools = await client.load_toolset( + bound_params={"param_P": "some_value"}, strict=True + ) + + assert len(tools) == 1 + assert tools[0].__name__ == TOOL_P + assert "param_P" not in tools[0].__signature__.parameters + @pytest.mark.asyncio async def test_load_toolset_strict_with_partially_used_bound_param_fail( self, mock_transport, tool_schema_with_param_P, tool_schema_minimal @@ -559,6 +581,29 @@ async def test_load_toolset_non_strict_with_unused_bound_param_fail( TOOLSET_NAME, bound_params={"param_Z": "some_value"} ) + @pytest.mark.asyncio + async def test_load_toolset_strict_with_fully_used_auth_success( + self, mock_transport, tool_schema_requires_auth_X + ): + """Tests that load_toolset succeeds in strict mode when every tool uses all auth tokens.""" + TOOL_AUTH = "tool_with_auth" + manifest = ManifestSchema( + serverVersion="0.0.0", + tools={TOOL_AUTH: tool_schema_requires_auth_X}, + ) + mock_transport.tools_list_mock.return_value = manifest + + async with ToolboxClient(TEST_BASE_URL) as client: + client._ToolboxClient__transport = mock_transport + tools = await client.load_toolset( + auth_token_getters={"auth_service_X": lambda: "token"}, strict=True + ) + + assert len(tools) == 1 + assert tools[0].__name__ == TOOL_AUTH + assert tools[0]._required_authn_params == {} + assert tools[0]._required_authz_tokens == () + @pytest.mark.asyncio async def test_load_toolset_strict_with_partially_used_auth_fail( self, mock_transport, tool_schema_requires_auth_X, tool_schema_minimal diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 6a6649b9f..b02df7a5e 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -232,6 +232,32 @@ def test_sync_load_toolset_success( assert result1 == f"{TOOL1_NAME}_ok" +def test_sync_load_toolset_strict_with_fully_used_bound_param_success( + sync_client, mock_transport +): + """Sync path: strict=True succeeds when every tool uses all bound params.""" + TOOL_P = "tool_with_p" + schema = ToolSchema( + description="Tool with Parameter P", + parameters=[ + ParameterSchema(name="param_P", type="string", description="Parameter P"), + ], + ) + manifest = ManifestSchema(serverVersion="0.0.0", tools={TOOL_P: schema}) + mock_transport.tools_list_mock.return_value = manifest + sync_client._ToolboxSyncClient__async_client._ToolboxClient__transport = ( + mock_transport + ) + + tools = sync_client.load_toolset( + bound_params={"param_P": "some_value"}, strict=True + ) + + assert len(tools) == 1 + assert tools[0].__name__ == TOOL_P + assert "param_P" not in tools[0].__signature__.parameters + + def test_sync_invoke_tool_server_error( test_tool_str_schema, sync_client, mock_transport ):