diff --git a/nanobot/agent/tools/self.py b/nanobot/agent/tools/self.py index 20fffa9d..f05dbf21 100644 --- a/nanobot/agent/tools/self.py +++ b/nanobot/agent/tools/self.py @@ -67,6 +67,13 @@ class MyTool(Tool): "private_key", "access_token", "refresh_token", "auth", }) + @classmethod + def _is_sensitive_field_name(cls, name: str) -> bool: + lowered = name.lower() + return lowered in cls._SENSITIVE_NAMES or any( + part in cls._SENSITIVE_NAMES for part in lowered.split("_") + ) + RESTRICTED: dict[str, dict[str, Any]] = { "max_iterations": {"type": int, "min": 1, "max": 100}, "context_window_tokens": {"type": int, "min": 4096, "max": 1_000_000}, @@ -248,13 +255,16 @@ class MyTool(Tool): return f"{key}: {r}" if key else r # Complex object — small Pydantic models: show values; others: show field names for navigation cls_name = type(val).__name__ - if hasattr(val, "model_fields"): - fields = list(val.model_fields.keys()) + model_fields = getattr(type(val), "model_fields", None) + if model_fields: + fields = list(model_fields.keys()) if len(fields) <= 8: # Small config objects: show field=value pairs pairs = [] for f in fields: fv = getattr(val, f, "?") + if MyTool._is_sensitive_field_name(f): + continue if isinstance(fv, (str, int, float, bool, type(None))): pairs.append(f"{f}={fv!r}") else: diff --git a/tests/agent/tools/test_self_tool.py b/tests/agent/tools/test_self_tool.py index f6ae4727..19b1639d 100644 --- a/tests/agent/tools/test_self_tool.py +++ b/tests/agent/tools/test_self_tool.py @@ -7,6 +7,7 @@ from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest +from pydantic import BaseModel from nanobot.agent.tools.self import MyTool @@ -168,6 +169,25 @@ class TestInspectPathNavigation: result = await tool.execute(action="check", key="tools") assert "not accessible" in result + @pytest.mark.asyncio + async def test_inspect_nested_config_redacts_sensitive_scalar_fields(self): + class SearchConfig(BaseModel): + provider: str = "tavily" + api_key: str = "sk-test-secret" + base_url: str = "" + max_results: int = 5 + + loop = _make_mock_loop() + loop.web_config = MagicMock() + loop.web_config.search = SearchConfig() + tool = _make_tool(loop) + + result = await tool.execute(action="check", key="web_config.search") + + assert "provider='tavily'" in result + assert "sk-test-secret" not in result + assert "api_key" not in result.lower() + # ---------------------------------------------------------------------------