From 10b52cfb3a03778a3efe2c4291eceaa8541b6f51 Mon Sep 17 00:00:00 2001 From: hamb1y Date: Thu, 2 Jul 2026 21:03:36 +0530 Subject: [PATCH] fix: resolve builtin skill reads --- nanobot/agent/tools/filesystem.py | 19 +++++++++++++++++++ tests/tools/test_filesystem_tools.py | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index 4a72d747..d0639902 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -194,6 +194,23 @@ def _is_blocked_device(path: str | Path) -> bool: return False +def _builtin_skill_read_path(path: str) -> Path | None: + """Map workspace-relative skills//... reads onto bundled skills.""" + from nanobot.agent.skills import BUILTIN_SKILLS_DIR + + requested = Path(path) + if requested.is_absolute(): + return None + parts = requested.parts + if len(parts) < 2 or parts[0] != "skills": + return None + root = BUILTIN_SKILLS_DIR.resolve() + candidate = (root / Path(*parts[1:])).resolve() + if candidate != root and root not in candidate.parents: + return None + return candidate if candidate.is_file() else None + + def _parse_page_range(pages: str, total: int) -> tuple[int, int]: """Parse a page range like '2-5' into 0-based (start, end) inclusive.""" parts = pages.strip().split("-") @@ -275,6 +292,8 @@ class ReadFileTool(_FsTool): return ToolResult.error(f"Error: Reading {path} is blocked (device path that could hang or produce infinite output).") fp = self._resolve_read(path) + if not fp.exists(): + fp = _builtin_skill_read_path(path) or fp if _is_blocked_device(fp): return ToolResult.error(f"Error: Reading {fp} is blocked (device path that could hang or produce infinite output).") if not fp.exists(): diff --git a/tests/tools/test_filesystem_tools.py b/tests/tools/test_filesystem_tools.py index 2df98776..efc2c420 100644 --- a/tests/tools/test_filesystem_tools.py +++ b/tests/tools/test_filesystem_tools.py @@ -77,6 +77,13 @@ class TestReadFileTool: assert "Error" in result assert "not found" in result + @pytest.mark.asyncio + async def test_workspace_relative_builtin_skill_read_falls_back_to_packaged_skill(self, tool): + result = await tool.execute(path="skills/long-goal/SKILL.md", limit=5) + + assert "Error" not in result + assert "long-goal" in result.lower() + @pytest.mark.asyncio async def test_missing_path_returns_clear_error(self, tool): result = await tool.execute()