Skip to content

feat: Support built-in toolset white list by --allowed-builtin-toolsets - #835

Closed
nilo19 wants to merge 1 commit into
HolmesGPT:masterfrom
nilo19:feat/allow-tools
Closed

nilo19 wants to merge 1 commit into
HolmesGPT:masterfrom
nilo19:feat/allow-tools

Conversation

@nilo19

@nilo19 nilo19 commented Aug 13, 2025

Copy link
Copy Markdown
Contributor

This is very useful when the majority of builtin toolsets are useless.

@CLAassistant

CLAassistant commented Aug 13, 2025 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Aug 13, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds an allowlist for builtin toolsets: Config gains allowed_builtin_toolsets with validation and CLI parsing; Config passes itself to ToolsetManager; ToolsetManager reads the allowlist and forwards it to load_builtin_toolsets, which now accepts and filters builtin toolsets. Tests added for unit and integration coverage.

Changes

Cohort / File(s) Summary
Config updates
holmes/config.py
Adds Config.allowed_builtin_toolsets: Optional[List[str]] and a @field_validator to trim/filter values; Config.load_from_file parses CSV CLI input; Config.toolset_manager constructs ToolsetManager(config=self, ...).
ToolsetManager wiring
holmes/core/toolset_manager.py
ToolsetManager.__init__ gains config (stored as _config); adds _get_allowed_builtin_toolsets() and forwards the allowlist to load_builtin_toolsets during builtin discovery.
CLI option propagation
holmes/main.py
Adds global option --allowed-builtin-toolsets and threads allowed_builtin_toolsets into Config.load_from_file across commands (ask, alertmanager, jira, github, pagerduty, opsgenie).
Builtin toolset loader
holmes/plugins/toolsets/...
load_builtin_toolsets signature extended with allowed_builtin_toolsets; logs warnings for unknown names and filters results to the requested set while preserving discovery order and toolset attributes.
Core tests
tests/core/test_config_allowed_builtin_toolsets.py, tests/core/test_toolset_manager_integration.py
Adds unit/integration tests for Config parsing/validation, ToolsetManager integration/caching, filtering semantics, and ensuring non-builtin toolsets remain unaffected.
Integration tests
tests/integration/test_allowed_builtin_toolsets_full.py
Adds extensive integration tests covering backward compatibility, malformed inputs, warnings for invalid names, order preservation, CLI parsing, caching, console/server lists, and robustness.
Plugin loader tests
tests/plugins/test_toolsets_filtering.py
Adds unit tests for load_builtin_toolsets filtering: None vs default, single/multiple/invalid names, duplicates, case-sensitivity, exact matching, order preservation, DAL interactions.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as holmes/main.py
  participant Config
  participant TM as ToolsetManager
  participant Loader as load_builtin_toolsets

  User->>CLI: run command (--allowed-builtin-toolsets=...)
  CLI->>Config: load_from_file(allowed_builtin_toolsets=CSV)
  Config-->>Config: parse/validate -> List[str] or None
  CLI->>Config: access config.toolset_manager
  Config->>TM: __init__(config=self, ...)
  TM->>TM: _get_allowed_builtin_toolsets()
  TM->>Loader: load_builtin_toolsets(..., allowed_builtin_toolsets)
  Loader-->>TM: filtered builtin toolsets
  TM-->>CLI: toolsets
  CLI-->>User: execute with resulting toolsets
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • moshemorad
  • arikalon1

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
holmes/plugins/toolsets/__init__.py (1)

102-105: Document the new parameter and clarify ordering semantics

The new allowed_builtin_toolsets parameter is clear from the code, but a short docstring would help future readers and avoid ambiguity about how ordering is handled (results follow builtin discovery order, not the allowlist’s order).

Consider adding a docstring like:

def load_builtin_toolsets(
    dal: Optional[SupabaseDal] = None,
    allowed_builtin_toolsets: Optional[List[str]] = None,
) -> List[Toolset]:
    """
    Load builtin (YAML + Python) toolsets.

    Args:
        dal: Supabase DAL for toolsets that need it.
        allowed_builtin_toolsets: If provided, restrict the loaded toolsets
            to names in this allowlist. Unknown names are warned and ignored.
            Note: the resulting order follows builtin discovery order, not the
            allowlist order.
    """
    ...
tests/plugins/test_toolsets_filtering.py (2)

96-99: Reduce timing flakiness; use perf_counter() for better resolution

Using time.time() can make this test flaky under load. perf_counter() is more appropriate for measuring durations, and a slightly looser threshold makes CI more stable.

Apply:

-    avg_baseline = sum(baseline_times) / len(baseline_times)
-    avg_filtered = sum(filtered_times) / len(filtered_times)
+    avg_baseline = sum(baseline_times) / len(baseline_times)
+    avg_filtered = sum(filtered_times) / len(filtered_times)
 
     # Should be within reasonable bounds (filtering adds minimal overhead)
-    # Using 3x multiplier to account for timing variations in test environment
-    assert avg_filtered < avg_baseline * 3.0
+    # Using a conservative multiplier to account for timing variations in CI
+    assert avg_filtered < avg_baseline * 4.0

And above, replace time.time() with time.perf_counter():

-        start = time.time()
+        start = time.perf_counter()
-        baseline_times.append(time.time() - start)
+        baseline_times.append(time.perf_counter() - start)
...
-        start = time.time()
+        start = time.perf_counter()
-        filtered_times.append(time.time() - start)
+        filtered_times.append(time.perf_counter() - start)

46-50: Also assert the warning for unknown names

Capturing the warning makes this test stronger and verifies the log contract.

You can enhance the test like this:

def test_load_builtin_toolsets_invalid_names(caplog):
    """Test that invalid toolset names are handled gracefully"""
    with caplog.at_level("WARNING"):
        filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
    assert len(filtered) == 0
    assert any("Unknown builtin toolsets specified" in rec.message for rec in caplog.records)
holmes/core/toolset_manager.py (1)

28-35: Prefer a Protocol over Any for config typing (avoids circular import and improves type-safety)

Using Any loses useful checks. A small Protocol avoids circular imports while documenting the expected shape.

Proposed pattern (outside this hunk):

from typing import Optional, List, Protocol

class _HasAllowedBuiltinToolsets(Protocol):
    allowed_builtin_toolsets: Optional[List[str]]

Then update the signature and attribute:

-    def __init__(
-        self,
-        config: Optional[Any] = None,  # Config instance
+    def __init__(
+        self,
+        config: Optional[_HasAllowedBuiltinToolsets] = None,  # Config-like instance
tests/cli/test_allowed_builtin_toolsets.py (1)

270-288: Fix unused loop variable

The loop variable test_case is not used within the loop body.

Apply this fix to address the static analysis warning:

-    for test_case in test_cases:
+    for _ in test_cases:
         # Just test that the option is parsed without error
         # We don't need to execute the full command
         result = runner.invoke(app, ["ask", "--help"])
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acb3ea2 and f663fb8925494e2af5046e5804133cd6f6307652.

📒 Files selected for processing (10)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/cli/__init__.py (1 hunks)
  • tests/cli/test_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • tests/integration/test_allowed_builtin_toolsets_full.py
  • tests/cli/__init__.py
  • tests/core/test_config_allowed_builtin_toolsets.py
  • holmes/plugins/toolsets/__init__.py
  • tests/plugins/test_toolsets_filtering.py
  • tests/core/test_toolset_manager_integration.py
  • holmes/config.py
  • holmes/core/toolset_manager.py
  • holmes/main.py
  • tests/cli/test_allowed_builtin_toolsets.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/integration/test_allowed_builtin_toolsets_full.py
  • tests/cli/__init__.py
  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/plugins/test_toolsets_filtering.py
  • tests/core/test_toolset_manager_integration.py
  • tests/cli/test_allowed_builtin_toolsets.py
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests
📚 Learning: 2025-08-10T06:02:54.321Z
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests

Applied to files:

  • tests/integration/test_allowed_builtin_toolsets_full.py
  • tests/plugins/test_toolsets_filtering.py
  • tests/core/test_toolset_manager_integration.py
  • tests/cli/test_allowed_builtin_toolsets.py
📚 Learning: 2025-07-08T08:45:41.069Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#610
File: .github/workflows/llm-evaluation.yaml:39-42
Timestamp: 2025-07-08T08:45:41.069Z
Learning: The robusta-dev/holmesgpt codebase has comprehensive existing validation for Azure environment variables (AZURE_API_BASE, AZURE_API_KEY, AZURE_API_VERSION) and MODEL in tests/llm/utils/classifiers.py, tests/llm/conftest.py, and holmes/core/llm.py. Don't suggest adding redundant validation logic.

Applied to files:

  • holmes/config.py
📚 Learning: 2025-06-24T05:51:04.543Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#535
File: holmes/plugins/toolsets/bash/bash_toolset.py:207-209
Timestamp: 2025-06-24T05:51:04.543Z
Learning: The init_config method in toolsets should be idempotent - safely callable multiple times without errors. self.config should maintain consistent typing (not alternate between dict and config object types) throughout the object lifecycle.

Applied to files:

  • holmes/core/toolset_manager.py
🧬 Code Graph Analysis (5)
tests/core/test_config_allowed_builtin_toolsets.py (1)
holmes/config.py (2)
  • Config (70-522)
  • load_from_file (190-227)
holmes/plugins/toolsets/__init__.py (1)
holmes/core/tools.py (1)
  • Toolset (333-475)
tests/plugins/test_toolsets_filtering.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-148)
tests/core/test_toolset_manager_integration.py (3)
holmes/config.py (1)
  • toolset_manager (147-156)
holmes/core/toolset_manager.py (5)
  • ToolsetManager (19-454)
  • _list_all_toolsets (76-143)
  • _get_allowed_builtin_toolsets (70-74)
  • list_console_toolsets (314-329)
  • list_server_toolsets (332-347)
holmes/core/tools.py (1)
  • ToolsetType (114-117)
holmes/core/toolset_manager.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-148)
🪛 Ruff (0.12.2)
tests/cli/test_allowed_builtin_toolsets.py

281-281: Loop control variable test_case not used within loop body

Rename unused test_case to _test_case

(B007)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Pre-commit checks
🔇 Additional comments (11)
tests/cli/__init__.py (1)

1-1: LGTM: package marker is fine

The package marker is appropriate and harmless.

holmes/core/toolset_manager.py (2)

70-75: LGTM: safe accessor for config-driven allowlist

The helper cleanly isolates config access and gracefully handles absence.


92-94: Wiring is correct; consider edge-case semantics when allowlist excludes overridden builtins

Passing allowed_builtin_toolsets into load_builtin_toolsets is correct. One edge case to verify: if a builtin toolset is excluded by allowlist but present in self.toolsets overrides, it will be treated as “custom” (type defaulted to CUSTOMIZED with strict_check=True), which may surprise users and/or fail validation. Confirm if that behavior is desired and covered by tests.

If you want to audit this scenario, add an integration test asserting the behavior when:

  • allowlist excludes “kubernetes/logs”
  • config overrides “kubernetes/logs: { enabled: true }”

Expected outcome: either

  • the override is ignored (preferred); or
  • it is treated as a custom toolset and validated strictly (current behavior).

I can draft the test if you confirm the intended semantics.

tests/integration/test_allowed_builtin_toolsets_full.py (1)

1-248: Comprehensive integration test coverage!

The integration tests provide thorough coverage of the allowed_builtin_toolsets feature, including backward compatibility, error handling, warning scenarios, and end-to-end flows. The test suite effectively validates the filtering behavior across different components.

holmes/config.py (3)

119-136: Well-designed field validator with proper semantics

The validator correctly implements the filtering logic, preserving the distinction between None (no filtering) and [] (filter everything), and handles whitespace gracefully.


149-150: Good design: Pass config reference to ToolsetManager

Passing self as the config parameter to ToolsetManager establishes proper dependency injection and enables the manager to access configuration settings like allowed_builtin_toolsets.


208-217: Proper CLI option parsing for comma-separated values

The implementation correctly parses the comma-separated string from CLI and handles whitespace, maintaining consistency with the field validator.

holmes/main.py (2)

96-100: Clear and helpful CLI option documentation

The option is well-documented with a clear help message and example usage.


182-183: Consistent propagation of the new option across all commands

The allowed_builtin_toolsets option is consistently added to all relevant investigate commands and properly passed through to Config.load_from_file.

Also applies to: 249-249, 381-381, 413-413, 520-520, 545-545, 713-713, 738-738, 800-800, 824-824, 888-888, 912-912

tests/core/test_toolset_manager_integration.py (1)

1-252: Excellent test coverage for ToolsetManager integration

The tests thoroughly validate the filtering behavior, backward compatibility, and integration between Config and ToolsetManager. The test structure is clear and covers all essential scenarios.

tests/cli/test_allowed_builtin_toolsets.py (1)

1-350: Comprehensive CLI testing with proper mocking

The tests thoroughly cover CLI option parsing, help text, and integration across all commands with appropriate mocking to isolate CLI behavior.

Comment thread holmes/plugins/toolsets/__init__.py Outdated
Comment thread tests/core/test_config_allowed_builtin_toolsets.py Outdated
Comment thread tests/core/test_config_allowed_builtin_toolsets.py Outdated
Comment thread tests/plugins/test_toolsets_filtering.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
holmes/core/toolset_manager.py (3)

36-37: Remove redundant assignment

self.toolsets is assigned twice in a row. Keep only the or {} form.

-        self.toolsets = toolsets
-        self.toolsets = toolsets or {}
+        self.toolsets = toolsets or {}

70-75: Defensive normalization of config value (optional)

If anything upstream accidentally passes a string (pre-parse) instead of list[str], normalize here defensively. Keeps ToolsetManager resilient to integration mis-wirings while still relying on Config to do the right thing.

     def _get_allowed_builtin_toolsets(self) -> Optional[List[str]]:
         """Get allowed builtin toolsets from config."""
-        if self._config is None:
-            return None
-        return getattr(self._config, "allowed_builtin_toolsets", None)
+        if self._config is None:
+            return None
+        value = getattr(self._config, "allowed_builtin_toolsets", None)
+        if isinstance(value, str):
+            # Accept comma-separated strings defensively
+            return [name.strip() for name in value.split(",") if name.strip()]
+        return value

453-455: Duplicate insertion into dict

The same dict assignment is performed twice; remove the duplicate line.

             else:
                 existing_toolsets_by_name[new_toolset.name] = new_toolset
-                existing_toolsets_by_name[new_toolset.name] = new_toolset
tests/cli/test_allowed_builtin_toolsets.py (1)

356-373: Reduce duplication: factor out a small helper to assert help text contains the option (optional)

Many tests repeat the same “option_present” block. Extracting a tiny helper will DRY the suite and ease future changes.

Example helper to add near the top of this file:

def _assert_help_contains_allowed_option(help_output: str) -> None:
    assert any(
        [
            "--allowed-builtin-toolsets" in help_output,
            "--allowed-builti" in help_output,  # truncated display resilience
            "allowed-builtin-toolsets" in help_output,
        ]
    ), f"Option not found in help output: {help_output}"

Then replace repeated blocks with:

help_output = result.stdout
_	assert_help_contains_allowed_option(help_output)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f663fb8925494e2af5046e5804133cd6f6307652 and 6011a1b4aa50379a31a61e7df83e2283c9a7cb95.

📒 Files selected for processing (10)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/cli/__init__.py (1 hunks)
  • tests/cli/test_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/cli/init.py
  • tests/plugins/test_toolsets_filtering.py
  • tests/integration/test_allowed_builtin_toolsets_full.py
  • holmes/plugins/toolsets/init.py
  • holmes/main.py
  • tests/core/test_config_allowed_builtin_toolsets.py
  • holmes/config.py
  • tests/core/test_toolset_manager_integration.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • tests/cli/test_allowed_builtin_toolsets.py
  • holmes/core/toolset_manager.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/cli/test_allowed_builtin_toolsets.py
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests
📚 Learning: 2025-08-10T06:02:54.321Z
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests

Applied to files:

  • tests/cli/test_allowed_builtin_toolsets.py
📚 Learning: 2025-06-24T05:51:04.543Z
Learnt from: nherment
PR: robusta-dev/holmesgpt#535
File: holmes/plugins/toolsets/bash/bash_toolset.py:207-209
Timestamp: 2025-06-24T05:51:04.543Z
Learning: The init_config method in toolsets should be idempotent - safely callable multiple times without errors. self.config should maintain consistent typing (not alternate between dict and config object types) throughout the object lifecycle.

Applied to files:

  • holmes/core/toolset_manager.py
🧬 Code Graph Analysis (1)
holmes/core/toolset_manager.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-158)
🪛 Ruff (0.12.2)
tests/cli/test_allowed_builtin_toolsets.py

338-338: Loop control variable test_case not used within loop body

Rename unused test_case to _test_case

(B007)

🔇 Additional comments (4)
holmes/core/toolset_manager.py (2)

92-94: Correctly threads allowlist into builtin loader

Good call to pass allowed_builtin_toolsets directly to load_builtin_toolsets, aligning ToolsetManager with Config/CLI.


26-35: No internal API break — making config keyword-only is unnecessary here

Repository scan shows all current instantiations either pass config by keyword or use no args; no positional-only usages were found that would be misinterpreted by moving config to the front.

Key call sites inspected (representative):

  • holmes/config.py:149-152 — ToolsetManager(config=self, toolsets=self.toolsets, mcp_servers=self.mcp_servers)
  • tests/core/test_toolset_manager.py:21 — ToolsetManager() (fixture)
  • tests/core/test_toolset_manager.py:336-339 — ToolsetManager(toolsets=None, mcp_servers=mcp_servers, custom_toolsets=None)
  • tests/integration/test_allowed_builtin_toolsets_full.py — multiple calls using ToolsetManager(config=...)

Conclusion: the suggested signature change is not required for this codebase; you can ignore the original critical warning. If ToolsetManager is a public API consumed externally, consider making config keyword-only in a deliberate, documented release to avoid surprising downstream users.

Likely an incorrect or invalid review comment.

tests/cli/test_allowed_builtin_toolsets.py (2)

8-24: Help coverage looks good

Validates the option is surfaced by the CLI and is resilient to truncated display; solid signal for CLI UX.


414-435: Nicely mirrors the parsing logic in Config

This test is a concise and faithful reproduction of the parsing behavior, catching regressions if the semantics change.

Comment thread tests/cli/test_allowed_builtin_toolsets.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
tests/plugins/test_toolsets_filtering.py (4)

6-13: Also assert order equality for None filter vs default

None filter should preserve not just contents but also discovery order. Add an explicit order assertion.

@@
     assert len(toolsets_none) == len(toolsets_default)
     assert {t.name for t in toolsets_none} == {t.name for t in toolsets_default}
+    # Order should be preserved as well
+    assert [t.name for t in toolsets_none] == [t.name for t in toolsets_default]

17-20: DRY: factor repeated “discover then skip if empty” into a fixture

You repeat the same pattern across many tests. A small fixture keeps tests concise and avoids repeated IO.

Add this fixture at module scope:

import pytest

@pytest.fixture(scope="module")
def builtin_toolsets_or_skip():
    toolsets = load_builtin_toolsets()
    if not toolsets:
        pytest.skip("No builtin toolsets available")
    return toolsets

Example refactor for one test:

-def test_load_builtin_toolsets_with_single_filter():
-    """Test filtering to single toolset"""
-    all_toolsets = load_builtin_toolsets()
-    if not all_toolsets:
-        pytest.skip("No builtin toolsets available")
+def test_load_builtin_toolsets_with_single_filter(builtin_toolsets_or_skip):
+    """Test filtering to single toolset"""
+    all_toolsets = builtin_toolsets_or_skip

You can apply the same pattern to the other tests listed in this comment’s line ranges.

Also applies to: 30-33, 56-58, 81-84, 103-105, 138-140, 153-155, 172-174, 185-187, 200-202


47-51: Assert a warning is emitted for invalid names to lock behavior

load_builtin_toolsets warns on unknown names. Asserting the warning prevents silent regressions.

Apply these diffs:

  1. Import logging at top:
@@
-import pytest
-import time
-from holmes.plugins.toolsets import load_builtin_toolsets
+import logging
+import pytest
+import time
+from holmes.plugins.toolsets import load_builtin_toolsets
  1. Update the test to use caplog:
-def test_load_builtin_toolsets_invalid_names():
-    """Test that invalid toolset names are handled gracefully"""
-    filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
-    assert len(filtered) == 0
+def test_load_builtin_toolsets_invalid_names(caplog):
+    """Test that invalid toolset names are handled gracefully"""
+    with caplog.at_level(logging.WARNING):
+        filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
+        assert len(filtered) == 0
+        # Ensure we logged about the unknown names
+        assert any("Unknown builtin toolsets" in rec.getMessage() for rec in caplog.records)

67-98: Reduce flakiness in the performance test (use perf_counter + tiny warm-up)

Use monotonic timing and a quick warm-up to stabilize timings across CI environments.

@@
-    # Baseline measurements
+    # Warm-up (avoid one-time import/cache effects)
+    load_builtin_toolsets()
+    # Baseline measurements
     baseline_times = []
     for _ in range(iterations):
-        start = time.time()
+        start = time.perf_counter()
         load_builtin_toolsets()
-        baseline_times.append(time.time() - start)
+        baseline_times.append(time.perf_counter() - start)
@@
     filtered_times = []
     for _ in range(iterations):
-        start = time.time()
+        start = time.perf_counter()
         load_builtin_toolsets(allowed_builtin_toolsets=[all_toolsets[0].name])
-        filtered_times.append(time.time() - start)
+        filtered_times.append(time.perf_counter() - start)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6011a1b4aa50379a31a61e7df83e2283c9a7cb95 and ae7daf5ce6d1928a6bbd27ae95798bc3c6fd5a98.

📒 Files selected for processing (8)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/core/test_toolset_manager_integration.py
  • tests/integration/test_allowed_builtin_toolsets_full.py
  • holmes/plugins/toolsets/init.py
  • holmes/main.py
  • holmes/core/toolset_manager.py
  • holmes/config.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • tests/plugins/test_toolsets_filtering.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/plugins/test_toolsets_filtering.py
🧠 Learnings (1)
📚 Learning: 2025-08-10T06:02:54.321Z
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests

Applied to files:

  • tests/plugins/test_toolsets_filtering.py
🧬 Code Graph Analysis (1)
tests/plugins/test_toolsets_filtering.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Pre-commit checks
🔇 Additional comments (4)
tests/plugins/test_toolsets_filtering.py (4)

1-3: Imports at module top: compliant with guidelines

Good fix. Imports are now module-level as required by project pre-commit rules.


100-116: Property preservation assertions are correct

Given loader behavior sets type and path for builtins uniformly, these checks guard against accidental mutation during filtering.


118-133: DAL parameter interop is covered well

Good that you validate dal=None path and ensure filtering still applies with dal explicitly passed.


197-211: Order preservation test is on point

Verifies discovery-order stability while filtering—aligned with the implementation using set membership but preserving original order.

Comment thread tests/plugins/test_toolsets_filtering.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (2)
holmes/main.py (2)

950-977: Expose the allowlist on toolset list/refresh commands

For consistency, let users pass the same flag when listing/refreshing toolsets. Today, only config-file values apply here; adding the CLI option improves parity with the other commands.

Apply:

 @toolset_app.command("list")
 def list_toolsets(
-    verbose: Optional[List[bool]] = opt_verbose,
-    config_file: Optional[Path] = opt_config_file,  # type: ignore
+    verbose: Optional[List[bool]] = opt_verbose,
+    config_file: Optional[Path] = opt_config_file,  # type: ignore
+    allowed_builtin_toolsets: Optional[str] = opt_allowed_builtin_toolsets,
 ):
@@
-    config = Config.load_from_file(config_file)
+    config = Config.load_from_file(
+        config_file, allowed_builtin_toolsets=allowed_builtin_toolsets
+    )
@@
 @toolset_app.command("refresh")
 def refresh_toolsets(
-    verbose: Optional[List[bool]] = opt_verbose,
-    config_file: Optional[Path] = opt_config_file,  # type: ignore
+    verbose: Optional[List[bool]] = opt_verbose,
+    config_file: Optional[Path] = opt_config_file,  # type: ignore
+    allowed_builtin_toolsets: Optional[str] = opt_allowed_builtin_toolsets,
 ):
@@
-    config = Config.load_from_file(config_file)
+    config = Config.load_from_file(
+        config_file, allowed_builtin_toolsets=allowed_builtin_toolsets
+    )

593-623: Add --allowed-builtin-toolsets to ticket command and forward it through SourceFactory.create_source

Short: Verified opt_allowed_builtin_toolsets exists (holmes/main.py:96) and is used by other commands, but the ticket command (holmes/main.py ~632) does not accept or forward it. SourceFactory.create_source (holmes/config.py ~535) also doesn't accept it and therefore doesn't pass it to Config.load_from_file. Config.load_from_file already handles allowed_builtin_toolsets, so we should thread the option through both places.

Files to change:

  • holmes/main.py — add CLI param to ticket() and forward to SourceFactory.create_source.
  • holmes/config.py — extend SourceFactory.create_source signature and pass allowed_builtin_toolsets into Config.load_from_file in both JIRA and PagerDuty branches.

Proposed diffs:

holmes/main.py (ticket command signature + call):

 def ticket(
@@
     post_processing_prompt: Optional[str] = opt_post_processing_prompt,
+    allowed_builtin_toolsets: Optional[str] = opt_allowed_builtin_toolsets,
 ):
@@
-        ticket_source = SourceFactory.create_source(
+        ticket_source = SourceFactory.create_source(
             source=source,
             config_file=config_file,
             ticket_url=ticket_url,
             ticket_username=ticket_username,
             ticket_api_key=ticket_api_key,
             ticket_id=ticket_id,
+            allowed_builtin_toolsets=allowed_builtin_toolsets,
         )

holmes/config.py (SourceFactory.create_source signature + Config.load_from_file calls):

 class SourceFactory(BaseModel):
     @staticmethod
     def create_source(
         source: SupportedTicketSources,
         config_file: Optional[Path],
         ticket_url: Optional[str],
         ticket_username: Optional[str],
         ticket_api_key: Optional[str],
         ticket_id: Optional[str],
+        allowed_builtin_toolsets: Optional[str] = None,
     ) -> TicketSource:
@@
-            config = Config.load_from_file(
+            config = Config.load_from_file(
                 config_file=config_file,
                 api_key=None,
                 model=None,
                 max_steps=None,
                 jira_url=ticket_url,
                 jira_username=ticket_username,
                 jira_api_key=ticket_api_key,
                 jira_query=None,
                 custom_toolsets=None,
                 custom_runbooks=None,
+                allowed_builtin_toolsets=allowed_builtin_toolsets,
             )
@@
-            config = Config.load_from_file(
+            config = Config.load_from_file(
                 config_file=config_file,
                 api_key=None,
                 model=None,
                 max_steps=None,
                 pagerduty_api_key=ticket_api_key,
                 pagerduty_user_email=ticket_username,
                 pagerduty_incident_key=None,
                 custom_toolsets=None,
                 custom_runbooks=None,
+                allowed_builtin_toolsets=allowed_builtin_toolsets,
             )

Notes:

  • SourceFactory.create_source is only used at holmes/main.py:632, so updating its signature is safe.
  • Config.load_from_file already parses the CLI string into a list (holmes/config.py lines ~208–214), so no change needed there.
🧹 Nitpick comments (4)
tests/core/test_config_allowed_builtin_toolsets.py (3)

29-49: Prefer parametrize over a for-loop for clearer failures

Using pytest.mark.parametrize will yield better per-case reporting when a case fails (you’ll see exactly which input failed), and aligns with pytest best practices.

Apply:

-def test_config_handle_whitespace(tmp_path):
-    """Test handling of whitespace and empty strings"""
-    # Create a minimal config file for testing CLI argument parsing
-    config_file = tmp_path / "config.yaml"
-    config_content = """
-# Test config for whitespace handling
-model: "gpt-4o"
-"""
-    config_file.write_text(config_content)
-
-    test_cases = [
-        ("kubernetes/core, prometheus/core", ["kubernetes/core", "prometheus/core"]),
-        ("kubernetes/core,  ,prometheus/core", ["kubernetes/core", "prometheus/core"]),
-        ("  kubernetes/core  ", ["kubernetes/core"]),
-        ("", []),
-    ]
-
-    for input_str, expected in test_cases:
-        # Use the public API to test the parsing logic
-        config = Config.load_from_file(config_file, allowed_builtin_toolsets=input_str)
-        assert config.allowed_builtin_toolsets == expected
+@pytest.mark.parametrize(
+    "input_str,expected",
+    [
+        ("kubernetes/core, prometheus/core", ["kubernetes/core", "prometheus/core"]),
+        ("kubernetes/core,  ,prometheus/core", ["kubernetes/core", "prometheus/core"]),
+        ("  kubernetes/core  ", ["kubernetes/core"]),
+        ("", []),
+    ],
+)
+def test_config_handle_whitespace(tmp_path, input_str, expected):
+    """Test handling of whitespace and empty strings"""
+    # Create a minimal config file for testing CLI argument parsing
+    config_file = tmp_path / "config.yaml"
+    config_file.write_text('model: "gpt-4o"\n')
+
+    # Use the public API to test the parsing logic
+    config = Config.load_from_file(config_file, allowed_builtin_toolsets=input_str)
+    assert config.allowed_builtin_toolsets == expected

Note: add import pytest at the top of this module if not present already.


4-8: Eliminate duplicate coverage for the default None case

Both tests assert the same behavior (attribute exists and defaults to None). Consolidate into a single test to reduce redundancy and speed up the suite.

Apply:

 def test_config_default_none():
     """Test that default value is None for backward compatibility"""
     config = Config()
-    assert config.allowed_builtin_toolsets is None
+    assert hasattr(config, "allowed_builtin_toolsets")
+    assert config.allowed_builtin_toolsets is None
@@
-def test_config_backward_compatibility():
-    """Test that existing config loading works unchanged"""
-    config = Config()
-    assert hasattr(config, "allowed_builtin_toolsets")
-    assert config.allowed_builtin_toolsets is None
+# Removed: duplicate of test_config_default_none

Also applies to: 52-57


119-131: Consider adding negative-type tests to lock down validation behavior

Current tests cover accepted types. It’s useful to assert that invalid types (e.g., int, dict, list with non-str) are rejected by Pydantic validators to prevent accidental regressions.

Example to add elsewhere in this file:

import pytest

@pytest.mark.parametrize("bad_value", [123, {"x": "y"}, [1, 2, 3], [None]])
def test_config_field_type_rejection(bad_value):
    with pytest.raises(Exception):
        Config(allowed_builtin_toolsets=bad_value)  # type: ignore[arg-type]
holmes/main.py (1)

96-100: Nit: prefer “built-in” spelling in help for consistency

Elsewhere (e.g., ToolsetType) the spelling is “built-in”. Aligning improves UX consistency.

Apply:

-opt_allowed_builtin_toolsets: Optional[str] = typer.Option(
+opt_allowed_builtin_toolsets: Optional[str] = typer.Option(
     None,
     "--allowed-builtin-toolsets",
-    help="Comma-separated list of builtin toolsets to allow (e.g., 'kubernetes/core,prometheus/core')",
+    help="Comma-separated list of built-in toolsets to allow (e.g., 'kubernetes/core,prometheus/core')",
 )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae7daf5ce6d1928a6bbd27ae95798bc3c6fd5a98 and 2392fe1c046b8191207653f14acbe219122a45e8.

📒 Files selected for processing (8)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/plugins/test_toolsets_filtering.py
  • holmes/plugins/toolsets/init.py
  • tests/core/test_toolset_manager_integration.py
  • holmes/config.py
  • holmes/core/toolset_manager.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/integration/test_allowed_builtin_toolsets_full.py
  • holmes/main.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/integration/test_allowed_builtin_toolsets_full.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests
📚 Learning: 2025-08-10T06:02:54.321Z
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests

Applied to files:

  • tests/integration/test_allowed_builtin_toolsets_full.py
🧬 Code Graph Analysis (2)
tests/core/test_config_allowed_builtin_toolsets.py (1)
holmes/config.py (2)
  • Config (70-522)
  • load_from_file (190-227)
tests/integration/test_allowed_builtin_toolsets_full.py (4)
holmes/config.py (3)
  • Config (70-522)
  • toolset_manager (147-156)
  • load_from_file (190-227)
holmes/core/toolset_manager.py (3)
  • _list_all_toolsets (76-143)
  • list_console_toolsets (314-329)
  • list_server_toolsets (332-347)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-158)
holmes/core/tools.py (1)
  • ToolsetType (114-117)
🔇 Additional comments (7)
tests/core/test_config_allowed_builtin_toolsets.py (2)

10-27: Good: exercising the real parsing path via Config.load_from_file

This test correctly drives the public API and avoids duplicating parsing logic. Creating a minimal config file and overriding via CLI kwargs ensures future parsing changes are covered.


59-75: LGTM: strong coverage on CLI parsing edge-cases

These independently validate handling of None, empty string, and whitespace via the real load_from_file merge path. This aligns with the intended None vs [] semantics in holmes/config.py.

holmes/main.py (2)

96-100: Add CLI allowlist: good defaulting and help

Option is Optional[str] with default None, so it won’t override config-file values. Help text is clear.


182-183: Propagation across commands looks correct

You thread the new option through the CLI signatures and into Config.load_from_file for ask, alertmanager, jira, github, pagerduty, and opsgenie. This matches the parsing contract in Config.load_from_file (string → list).

Also applies to: 243-252, 381-414, 520-546, 713-739, 800-825, 888-913

tests/integration/test_allowed_builtin_toolsets_full.py (3)

136-156: LGTM: order preservation test mirrors loader behavior

Verifies that filtering does not reorder the source discovery order. This matches load_builtin_toolsets semantics.


168-184: LGTM: empty filter semantics validated

Confirms [] ⇒ no built-ins (distinct from None). Non-built-ins aren’t constrained by this feature.


226-248: Solid robustness coverage across edge cases

Covers None vs [] vs duplicates vs case sensitivity vs extra path components, ensuring no crashes on odd inputs.

Comment thread tests/integration/test_allowed_builtin_toolsets_full.py Outdated
Comment thread tests/integration/test_allowed_builtin_toolsets_full.py Outdated
Comment thread tests/integration/test_allowed_builtin_toolsets_full.py Outdated
Comment thread tests/integration/test_allowed_builtin_toolsets_full.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
holmes/plugins/toolsets/__init__.py (2)

139-141: Standardize spelling: “built-in” (hyphenated) in logs

Consistency nit: elsewhere the enum uses “built-in” (ToolsetType.BUILTIN.value == "built-in"). Consider standardizing log strings.

Apply within this hunk:

-            logging.warning(
-                f"Unknown builtin toolsets specified: {sorted(invalid_names)}"
-            )
+            logging.warning(
+                f"Unknown built-in toolsets specified: {sorted(invalid_names)}"
+            )

106-113: Nit: Align docstring wording with enum (“built-in” vs “builtin”)

Minor consistency update for the docstring to match ToolsetType naming.

-    Load builtin toolsets from YAML files and Python modules.
+    Load built-in toolsets from YAML files and Python modules.
holmes/main.py (1)

96-100: Nit: Use “built-in” (hyphenated) in CLI help for consistency

Aligns user-facing help with enum spelling and logs.

-opt_allowed_builtin_toolsets: Optional[str] = typer.Option(
+opt_allowed_builtin_toolsets: Optional[str] = typer.Option(
     None,
     "--allowed-builtin-toolsets",
-    help="Comma-separated list of builtin toolsets to allow (e.g., 'kubernetes/core,prometheus/core')",
+    help="Comma-separated list of built-in toolsets to allow (e.g., 'kubernetes/core,prometheus/core')",
 )
🧹 Nitpick comments (4)
tests/plugins/test_toolsets_filtering.py (2)

46-50: Also assert the warning is emitted for invalid toolset names

Strengthen this test by verifying the warning log when invalid names are passed.

-def test_load_builtin_toolsets_invalid_names():
+def test_load_builtin_toolsets_invalid_names(caplog):
     """Test that invalid toolset names are handled gracefully"""
-    filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
+    with caplog.at_level("WARNING"):
+        filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
     assert len(filtered) == 0
+    # Adjust the expected substring if you keep “builtin” instead of “built-in”
+    assert any("Unknown built-in toolsets specified" in m for m in caplog.messages)

66-82: Assert explicit invariants (type and path) for filtered built-ins

Since built-in toolsets are marked with ToolsetType.BUILTIN and path is hidden, assert these invariants directly in addition to property equality.

-import pytest
-from holmes.plugins.toolsets import load_builtin_toolsets
+import pytest
+from holmes.core.tools import ToolsetType
+from holmes.plugins.toolsets import load_builtin_toolsets
@@
     # Check that properties are preserved
     assert filtered_toolset.name == target_toolset.name
-    assert filtered_toolset.type == target_toolset.type
-    assert filtered_toolset.path == target_toolset.path
+    assert filtered_toolset.type == target_toolset.type
+    assert filtered_toolset.path == target_toolset.path
+    # Built-in invariants
+    assert filtered_toolset.type == ToolsetType.BUILTIN
+    assert filtered_toolset.path is None
tests/core/test_toolset_manager_integration.py (2)

91-101: Also assert that a warning was logged for invalid names

This ensures the warning path is covered in integration, not only at the plugin level.

-def test_toolset_manager_invalid_names():
+def test_toolset_manager_invalid_names(caplog):
     """Test ToolsetManager with invalid toolset names"""
     config = Config(allowed_builtin_toolsets=["nonexistent/toolset"])
     manager = ToolsetManager(config=config)
 
-    toolsets = manager._list_all_toolsets()
+    with caplog.at_level("WARNING"):
+        toolsets = manager._list_all_toolsets()
     builtin_toolsets = [t for t in toolsets if t.type == ToolsetType.BUILTIN]
 
     # Should have no builtin toolsets (invalid names filtered out)
     assert len(builtin_toolsets) == 0
+    # Adjust the expected substring if you keep “builtin” instead of “built-in”
+    assert any("Unknown built-in toolsets specified" in m for m in caplog.messages)

103-118: Use of private method in tests may be brittle

Multiple tests call _list_all_toolsets(), which is private API. If a public aggregator exists, prefer it to reduce coupling to internals; otherwise, keep as-is with the understanding it may require updates on refactors.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2392fe1c046b8191207653f14acbe219122a45e8 and f92ea9d07bf6b3609ae053779239d510d8b643ef.

📒 Files selected for processing (8)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/integration/test_allowed_builtin_toolsets_full.py
  • holmes/config.py
  • holmes/core/toolset_manager.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • holmes/plugins/toolsets/__init__.py
  • tests/core/test_toolset_manager_integration.py
  • holmes/main.py
  • tests/plugins/test_toolsets_filtering.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/core/test_toolset_manager_integration.py
  • tests/plugins/test_toolsets_filtering.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests
📚 Learning: 2025-08-10T06:02:54.321Z
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests

Applied to files:

  • tests/core/test_toolset_manager_integration.py
  • tests/plugins/test_toolsets_filtering.py
🧬 Code Graph Analysis (2)
holmes/plugins/toolsets/__init__.py (1)
holmes/core/tools.py (2)
  • Toolset (333-475)
  • ToolsetType (114-117)
tests/plugins/test_toolsets_filtering.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-158)
🔇 Additional comments (6)
holmes/plugins/toolsets/__init__.py (3)

102-113: Docstring explicitly states order semantics — good clarity

The docstring clearly documents that filtering preserves discovery order. This avoids ambiguity around ordering guarantees.


132-151: Filtering logic is correct and preserves discovery order

  • Efficient set-based check
  • Warns on unknown names
  • Preserves discovery order by design (and as documented)

152-158: Correctly marks toolsets as built-in and hides paths

Setting ToolsetType and clearing path is aligned with the model’s semantics and security expectations.

tests/plugins/test_toolsets_filtering.py (1)

163-176: Order preservation test is solid

Good coverage verifying that filtering maintains loader (discovery) order.

holmes/main.py (1)

182-183: No change needed — Config.load_from_file already parses CSV into List[str]

Confirmed: holmes/config.py defines allowed_builtin_toolsets as Optional[List[str]] and load_from_file converts a CSV string into a List[str]; ToolsetManager accepts Optional[List[str]] and forwards it to load_builtin_toolsets.

Affected locations:

  • holmes/config.py
    • allowed_builtin_toolsets declaration & validator: ~115–127
    • load_from_file signature: ~188–190
    • CSV parsing in load_from_file: ~206–216
  • holmes/core/toolset_manager.py
    • _get_allowed_builtin_toolsets definition: ~70–74
    • usage when loading builtin toolsets: ~91–94

No CLI-layer parsing change required.

tests/core/test_toolset_manager_integration.py (1)

148-158: Config.toolset_manager caching test is good

Verifies property-level caching behavior with identity checks.

Comment thread tests/core/test_toolset_manager_integration.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
tests/core/test_config_allowed_builtin_toolsets.py (1)

59-132: Add precedence tests: CLI should override file; empty CLI value should override to []

Strengthen coverage by verifying merge behavior between file and CLI inputs.

Consider adding this test to the module:

def test_config_cli_overrides_file(tmp_path):
    # File sets a value; CLI should override it
    config_file = tmp_path / "config.yaml"
    config_file.write_text('allowed_builtin_toolsets: ["file/toolset"]\n')

    cfg = Config.load_from_file(config_file, allowed_builtin_toolsets="cli/toolset")
    assert cfg.allowed_builtin_toolsets == ["cli/toolset"]

def test_config_cli_empty_string_overrides_file_to_empty(tmp_path):
    # File sets a value; CLI empty string should override to []
    config_file = tmp_path / "config.yaml"
    config_file.write_text('allowed_builtin_toolsets: ["file/toolset"]\n')

    cfg = Config.load_from_file(config_file, allowed_builtin_toolsets="")
    assert cfg.allowed_builtin_toolsets == []

If you want, I can open a follow-up PR adding these tests.

🧹 Nitpick comments (4)
tests/core/test_config_allowed_builtin_toolsets.py (2)

29-50: Parametrize the whitespace parsing cases for clearer failures and less boilerplate

Use pytest.parametrize instead of a manual loop; each case will then be reported independently with better diagnostics.

Apply:

+import pytest
@@
-def test_config_handle_whitespace(tmp_path):
-    """Test handling of whitespace and empty strings"""
-    # Create a minimal config file for testing CLI argument parsing
-    config_file = tmp_path / "config.yaml"
-    config_content = """
-# Test config for whitespace handling
-model: "gpt-4o"
-"""
-    config_file.write_text(config_content)
-
-    test_cases = [
-        ("kubernetes/core, prometheus/core", ["kubernetes/core", "prometheus/core"]),
-        ("kubernetes/core,  ,prometheus/core", ["kubernetes/core", "prometheus/core"]),
-        ("  kubernetes/core  ", ["kubernetes/core"]),
-        ("", []),
-    ]
-
-    for input_str, expected in test_cases:
-        # Use the public API to test the parsing logic
-        config = Config.load_from_file(config_file, allowed_builtin_toolsets=input_str)
-        assert config.allowed_builtin_toolsets == expected
+@pytest.mark.parametrize(
+    "input_str,expected",
+    [
+        ("kubernetes/core, prometheus/core", ["kubernetes/core", "prometheus/core"]),
+        ("kubernetes/core,  ,prometheus/core", ["kubernetes/core", "prometheus/core"]),
+        ("  kubernetes/core  ", ["kubernetes/core"]),
+        ("", []),
+    ],
+)
+def test_config_handle_whitespace(tmp_path, input_str, expected):
+    """Test handling of whitespace and empty strings"""
+    # Create a minimal config file for testing CLI argument parsing
+    config_file = tmp_path / "config.yaml"
+    config_content = """
+# Test config for whitespace handling
+model: "gpt-4o"
+"""
+    config_file.write_text(config_content)
+
+    # Use the public API to test the parsing logic
+    config = Config.load_from_file(config_file, allowed_builtin_toolsets=input_str)
+    assert config.allowed_builtin_toolsets == expected

Note: Add the new top-level import if not already present.


52-57: Deduplicate: Merge this with test_config_default_none

This test repeats the same assertion as Lines 4–8. Fold the hasattr check into the first test and remove this function.

Apply:

-def test_config_backward_compatibility():
-    """Test that existing config loading works unchanged"""
-    config = Config()
-    assert hasattr(config, "allowed_builtin_toolsets")
-    assert config.allowed_builtin_toolsets is None
+def test_config_default_none():
+    """Test that default value is None for backward compatibility"""
+    config = Config()
+    assert hasattr(config, "allowed_builtin_toolsets")
+    assert config.allowed_builtin_toolsets is None
tests/plugins/test_toolsets_filtering.py (2)

46-50: Also assert the warning for invalid names using caplog

The implementation logs a warning for unknown names. Capture and assert it to lock the behavior.

Apply:

-def test_load_builtin_toolsets_invalid_names():
+def test_load_builtin_toolsets_invalid_names(caplog):
     """Test that invalid toolset names are handled gracefully"""
-    filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
+    with caplog.at_level("WARNING"):
+        filtered = load_builtin_toolsets(
+            allowed_builtin_toolsets=["nonexistent/toolset"]
+        )
     assert len(filtered) == 0
+    assert any(
+        "Unknown builtin toolsets specified" in r.message for r in caplog.records
+    )

66-82: Strengthen property check: assert path is not exposed

Per implementation, builtin toolsets should not expose a path. Assert this explicitly.

Apply:

     filtered_toolset = filtered[0]
@@
     assert filtered_toolset.type == target_toolset.type
-    assert filtered_toolset.path == target_toolset.path
+    assert filtered_toolset.path == target_toolset.path
+    # Builtin toolsets must not expose a path
+    assert filtered_toolset.path is None
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f92ea9d07bf6b3609ae053779239d510d8b643ef and 88e2ee15dac312889bfd3e25168450b25be44fde.

📒 Files selected for processing (8)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/integration/test_allowed_builtin_toolsets_full.py
  • holmes/main.py
  • holmes/core/toolset_manager.py
  • holmes/plugins/toolsets/init.py
  • tests/core/test_toolset_manager_integration.py
  • holmes/config.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/plugins/test_toolsets_filtering.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/core/test_config_allowed_builtin_toolsets.py
  • tests/plugins/test_toolsets_filtering.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests
📚 Learning: 2025-08-10T06:02:54.321Z
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests

Applied to files:

  • tests/plugins/test_toolsets_filtering.py
🧬 Code Graph Analysis (2)
tests/core/test_config_allowed_builtin_toolsets.py (1)
holmes/config.py (2)
  • Config (70-522)
  • load_from_file (190-227)
tests/plugins/test_toolsets_filtering.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-158)
🔇 Additional comments (11)
tests/core/test_config_allowed_builtin_toolsets.py (2)

4-8: LGTM: Backward compatibility default is explicit and correct

Defaulting to None is the right choice to preserve existing behavior.


10-27: Good: Test exercises real parsing path via Config.load_from_file

Driving the check through the actual API (with a temp file present) avoids duplicating parsing logic. Well done.

tests/plugins/test_toolsets_filtering.py (9)

5-12: LGTM: No-filter behavior parity

Ensures None matches default discovery; correct baseline.


14-25: LGTM: Single-name filtering

Basic selection path is covered; skip guard is appropriate when no toolsets exist.


40-44: LGTM: Empty filter returns no toolsets

Clear and correct expectation.


52-64: LGTM: Mixed valid/invalid names

Correctly ensures only valid matches survive.


84-99: LGTM: dal parameter interplay

Covers dal=None baseline and filtered path. Looks good.


101-114: LGTM: Case-sensitive matching

Good negative test to prevent accidental case-folding.


116-133: LGTM: Exact-name matching

Protects against substring matches; solid.


135-146: LGTM: All-valid allowlist returns all

Useful parity check with baseline discovery.


148-161: LGTM: Duplicate names don’t duplicate results

Confirms idempotence; nice catch.

Comment on lines 27 to 40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make order assertion robust: reverse the allowlist to catch reordering

As written, the allowlist order matches discovery order, so the test can’t detect an implementation that reorders by the allowlist. Reverse the input and assert discovery order in the output.

Apply:

-    target_names = [all_toolsets[0].name, all_toolsets[1].name]
+    # Reverse order to ensure filtering preserves discovery order rather than allowlist order
+    target_names = [all_toolsets[1].name, all_toolsets[0].name]
     filtered = load_builtin_toolsets(allowed_builtin_toolsets=target_names)
 
-    assert len(filtered) == 2
-    assert {t.name for t in filtered} == set(target_names)
+    assert len(filtered) == 2
+    # Names should match, but order should remain discovery order
+    assert [t.name for t in filtered] == [all_toolsets[0].name, all_toolsets[1].name]
🤖 Prompt for AI Agents
In tests/plugins/test_toolsets_filtering.py around lines 27 to 38, the test
currently uses an allowlist in the same order as discovery so it can't detect
reordering; change target_names to use the reverse of the two chosen builtin
toolset names before calling
load_builtin_toolsets(allowed_builtin_toolsets=...), then assert the returned
filtered list preserves discovery order by comparing [t.name for t in filtered]
== target_names (keep or remove the set-based assertion if redundant). Ensure
the test still skips when <2 toolsets.

Comment thread tests/plugins/test_toolsets_filtering.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (8)
holmes/plugins/toolsets/__init__.py (3)

106-113: Docstring clarity is good; consider making discovery order deterministic

The order semantics are clearly documented. To ensure determinism across platforms/filesystems, consider sorting the YAML filenames so that “discovery order” is stable.

Outside the selected lines, update the loop to sort filenames:

for filename in sorted(os.listdir(THIS_DIR)):
    ...

138-141: Nit: friendlier, structured warning message

Use logger parameterization and a comma-separated list for readability.

-        if invalid_names:
-            logging.warning(
-                f"Unknown builtin toolsets specified: {sorted(invalid_names)}"
-            )
+        if invalid_names:
+            unknown = ", ".join(sorted(invalid_names))
+            logging.warning("Unknown builtin toolsets specified: %s", unknown)

152-156: Clarify misleading comment or enforce it explicitly

The comment states “disable builtin toolsets by default,” but the code doesn’t change the enabled flag. Either clarify the comment to reflect what’s actually done, or explicitly set enabled = False.

Clarify the comment:

-    # disable builtin toolsets by default, and the user can enable them explicitly in config.
+    # Mark builtin toolsets and hide their internal path; enabling remains explicit in user config.

If you prefer enforcing the invariant:

for toolset in all_toolsets:
    toolset.type = ToolsetType.BUILTIN
    toolset.enabled = False
    toolset.path = None
tests/plugins/test_toolsets_filtering.py (5)

1-2: Import ToolsetType to assert intended invariants (not just “same as target”)

Asserting explicit invariants makes the test more robust than comparing two instances that are both post-processed by the loader.

 import pytest
+from holmes.core.tools import ToolsetType
 from holmes.plugins.toolsets import load_builtin_toolsets

48-52: Assert warning is emitted for invalid toolsets

Capture logs to ensure an explicit warning is emitted when unknown names are provided.

-def test_load_builtin_toolsets_invalid_names():
+def test_load_builtin_toolsets_invalid_names(caplog):
     """Test that invalid toolset names are handled gracefully"""
-    filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
+    with caplog.at_level("WARNING"):
+        filtered = load_builtin_toolsets(allowed_builtin_toolsets=["nonexistent/toolset"])
     assert len(filtered) == 0
+    assert "Unknown builtin toolsets specified" in caplog.text

80-84: Assert invariants explicitly (BUILTIN type and hidden path)

Strengthen the test to check the contract stated by the loader (type set to BUILTIN, path cleared), not just equality to another post-processed instance.

-    # Check that properties are preserved
-    assert filtered_toolset.name == target_toolset.name
-    assert filtered_toolset.type == target_toolset.type
-    assert filtered_toolset.path == target_toolset.path
+    # Check invariants on the filtered toolset
+    assert filtered_toolset.name == target_toolset.name
+    assert filtered_toolset.type == ToolsetType.BUILTIN
+    assert filtered_toolset.path is None

137-148: Also assert order is preserved when all names are allowed

This catches accidental reordering when the allowlist contains all toolsets.

     assert len(filtered) == len(all_toolsets)
     assert {t.name for t in filtered} == {t.name for t in all_toolsets}
+    assert [t.name for t in filtered] == [t.name for t in all_toolsets]

5-176: Reduce test runtime by reusing a cached list of toolsets

Multiple tests repeatedly call load_builtin_toolsets(), which reloads and instantiates all toolsets each time. Introduce a module-scoped fixture to cache results and pass it to tests that don’t depend on side-effects.

Example (apply outside this hunk):

import pytest
from holmes.plugins.toolsets import load_builtin_toolsets

@pytest.fixture(scope="module")
def builtin_toolsets():
    ts = load_builtin_toolsets()
    if not ts:
        pytest.skip("No builtin toolsets available")
    return ts

def test_load_builtin_toolsets_with_single_filter(builtin_toolsets):
    first_toolset_name = builtin_toolsets[0].name
    filtered = load_builtin_toolsets(allowed_builtin_toolsets=[first_toolset_name])
    assert len(filtered) == 1
    assert filtered[0].name == first_toolset_name
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88e2ee15dac312889bfd3e25168450b25be44fde and 482e102.

📒 Files selected for processing (8)
  • holmes/config.py (4 hunks)
  • holmes/core/toolset_manager.py (3 hunks)
  • holmes/main.py (13 hunks)
  • holmes/plugins/toolsets/__init__.py (2 hunks)
  • tests/core/test_config_allowed_builtin_toolsets.py (1 hunks)
  • tests/core/test_toolset_manager_integration.py (1 hunks)
  • tests/integration/test_allowed_builtin_toolsets_full.py (1 hunks)
  • tests/plugins/test_toolsets_filtering.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • holmes/main.py
  • tests/core/test_toolset_manager_integration.py
  • tests/integration/test_allowed_builtin_toolsets_full.py
  • holmes/config.py
  • tests/core/test_config_allowed_builtin_toolsets.py
  • holmes/core/toolset_manager.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.py: ALWAYS place Python imports at the top of the file, not inside functions or methods
Use Ruff for formatting and linting (configured in pyproject.toml)
Type hints required (mypy configuration in pyproject.toml)
Pre-commit hooks enforce quality checks
Don't add convenience logs that give away the problem
Don't write logs that directly state the issue
Ensure historical timestamps are properly handled in logs (especially with Loki)

Files:

  • tests/plugins/test_toolsets_filtering.py
  • holmes/plugins/toolsets/__init__.py
tests/**

📄 CodeRabbit Inference Engine (CLAUDE.md)

Tests must match source structure under tests/

Files:

  • tests/plugins/test_toolsets_filtering.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
PR: robusta-dev/holmesgpt#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T06:02:54.321Z
Learning: New toolsets require integration tests
🧬 Code Graph Analysis (2)
tests/plugins/test_toolsets_filtering.py (1)
holmes/plugins/toolsets/__init__.py (1)
  • load_builtin_toolsets (102-158)
holmes/plugins/toolsets/__init__.py (1)
holmes/core/tools.py (2)
  • Toolset (333-475)
  • ToolsetType (114-117)

Comment on lines +131 to 151
# Apply filter if specified
if allowed_builtin_toolsets is not None:
# Convert to set for O(1) lookup performance
requested_names = set(allowed_builtin_toolsets)
actual_names = {toolset.name for toolset in all_toolsets}
invalid_names = requested_names - actual_names

if invalid_names:
logging.warning(
f"Unknown builtin toolsets specified: {sorted(invalid_names)}"
)

# Filter to only allowed toolsets using set for efficient lookup
# Note: This preserves the original discovery order rather than
# reordering by the allowlist
all_toolsets = [
toolset
for toolset in all_toolsets
if toolset.name in requested_names # O(1) lookup
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Allowlist still loads/instantiates all toolsets; short-circuit and avoid unnecessary work

Currently, all YAML and Python toolsets are loaded/instantiated and only then filtered. This undermines the goal of speeding up scenarios where most builtins aren’t needed.

Two concrete improvements:

  • Early return for an empty allowlist (fast path).
  • After loading YAML toolsets, if the allowlist is fully satisfied by YAML names, skip constructing Python toolsets altogether.

Apply a minimal fast-path within this hunk:

     # Apply filter if specified
     if allowed_builtin_toolsets is not None:
+        # Fast path: empty allowlist means nothing to load
+        if len(allowed_builtin_toolsets) == 0:
+            return []
         # Convert to set for O(1) lookup performance
         requested_names = set(allowed_builtin_toolsets)

Outside the selected lines, consider this structure to skip Python toolsets when not needed:

def load_builtin_toolsets(
    dal: Optional[SupabaseDal] = None,
    allowed_builtin_toolsets: Optional[List[str]] = None,
) -> List[Toolset]:
    logging.debug(f"loading toolsets from {THIS_DIR}")
    if allowed_builtin_toolsets is not None:
        requested = set(allowed_builtin_toolsets)
        if not requested:
            return []

    # Load YAML toolsets first (sorted for determinism)
    all_toolsets: List[Toolset] = []
    for filename in sorted(os.listdir(THIS_DIR)):
        if not filename.endswith(".yaml"):
            continue
        if filename == "kubernetes_logs.yaml" and not USE_LEGACY_KUBERNETES_LOGS:
            continue
        all_toolsets.extend(load_toolsets_from_file(os.path.join(THIS_DIR, filename), strict_check=True))

    # If an allowlist is provided and all requested names are covered by YAML, skip Python loading
    if allowed_builtin_toolsets is not None:
        yaml_names = {t.name for t in all_toolsets}
        if requested.issubset(yaml_names):
            filtered = [t for t in all_toolsets if t.name in requested]
            for t in filtered:
                t.type = ToolsetType.BUILTIN
                t.path = None
            return filtered

    # Otherwise, load Python toolsets and filter as today
    all_toolsets.extend(load_python_toolsets(dal=dal))
    if allowed_builtin_toolsets is not None:
        requested = set(allowed_builtin_toolsets)
        invalid = requested - {t.name for t in all_toolsets}
        if invalid:
            logging.warning("Unknown builtin toolsets specified: %s", ", ".join(sorted(invalid)))
        all_toolsets = [t for t in all_toolsets if t.name in requested]

    for t in all_toolsets:
        t.type = ToolsetType.BUILTIN
        t.path = None
    return all_toolsets
🤖 Prompt for AI Agents
In holmes/plugins/toolsets/__init__.py around lines 131 to 151, the allowlist is
applied after all YAML and Python toolsets are loaded which still instantiates
everything; change the logic to (1) early-return [] when
allowed_builtin_toolsets is provided as an empty list, (2) load YAML toolsets
first, build requested = set(allowed_builtin_toolsets) and if requested is a
subset of the loaded YAML names then filter those YAML toolsets, set each
filtered toolset's type to ToolsetType.BUILTIN and path to None, and return the
filtered list without loading Python toolsets, and (3) otherwise proceed to load
Python toolsets and perform the existing allowlist filtering and unknown-name
logging; ensure lookups use sets for O(1) checks and preserve discovery order
when filtering.

@nilo19

nilo19 commented Aug 14, 2025

Copy link
Copy Markdown
Contributor Author

/close

@nilo19 nilo19 closed this Aug 14, 2025
@nilo19
nilo19 deleted the feat/allow-tools branch August 14, 2025 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants