From 45832ea499907a701913faa49fbded384abc1339 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 15 Mar 2026 07:18:20 +0000 Subject: [PATCH] Add load_skill tool to bypass workspace restriction for builtin skills When restrictToWorkspace is enabled, the agent cannot read builtin skill files via read_file since they live outside the workspace. This adds a dedicated load_skill tool that reads skills by name through the SkillsLoader, which accesses files directly via Python without the workspace restriction. - Add LoadSkillTool to filesystem tools - Register it in the agent loop - Update system prompt to instruct agent to use load_skill instead of read_file - Remove raw filesystem paths from skills summary --- nanobot/agent/context.py | 2 +- nanobot/agent/loop.py | 3 ++- nanobot/agent/skills.py | 2 -- nanobot/agent/subagent.py | 2 +- nanobot/agent/tools/filesystem.py | 32 +++++++++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index e47fcb8..a6c3eea 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -46,7 +46,7 @@ class ContextBuilder: if skills_summary: parts.append(f"""# Skills -The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool. +The following skills extend your capabilities. To use a skill, call the load_skill tool with its name. Skills with available="false" need dependencies installed first - you can try installing them with apt/brew. {skills_summary}""") diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index d644845..9cbdaf8 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -17,7 +17,7 @@ from nanobot.agent.context import ContextBuilder from nanobot.agent.memory import MemoryConsolidator from nanobot.agent.subagent import SubagentManager from nanobot.agent.tools.cron import CronTool -from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool +from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, LoadSkillTool, ReadFileTool, WriteFileTool from nanobot.agent.tools.message import MessageTool from nanobot.agent.tools.registry import ToolRegistry from nanobot.agent.tools.shell import ExecTool @@ -128,6 +128,7 @@ class AgentLoop: self.tools.register(SpawnTool(manager=self.subagents)) if self.cron_service: self.tools.register(CronTool(self.cron_service)) + self.tools.register(LoadSkillTool(skills_loader=self.context.skills)) async def _connect_mcp(self) -> None: """Connect to configured MCP servers (one-time, lazy).""" diff --git a/nanobot/agent/skills.py b/nanobot/agent/skills.py index 9afee82..2b869fa 100644 --- a/nanobot/agent/skills.py +++ b/nanobot/agent/skills.py @@ -118,7 +118,6 @@ class SkillsLoader: lines = [""] for s in all_skills: name = escape_xml(s["name"]) - path = s["path"] desc = escape_xml(self._get_skill_description(s["name"])) skill_meta = self._get_skill_meta(s["name"]) available = self._check_requirements(skill_meta) @@ -126,7 +125,6 @@ class SkillsLoader: lines.append(f" ") lines.append(f" {name}") lines.append(f" {desc}") - lines.append(f" {path}") # Show missing requirements for unavailable skills if not available: diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index b6bef68..cdde30d 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -213,7 +213,7 @@ Stay focused on the assigned task. Your final response will be reported back to skills_summary = SkillsLoader(self.workspace).build_skills_summary() if skills_summary: - parts.append(f"## Skills\n\nRead SKILL.md with read_file to use a skill.\n\n{skills_summary}") + parts.append(f"## Skills\n\nUse load_skill tool to load a skill by name.\n\n{skills_summary}") return "\n\n".join(parts) diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index 02c8331..95bc980 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -363,3 +363,35 @@ class ListDirTool(_FsTool): return f"Error: {e}" except Exception as e: return f"Error listing directory: {e}" + + +class LoadSkillTool(Tool): + """Tool to load a skill by name, bypassing workspace restriction.""" + + def __init__(self, skills_loader): + self._skills_loader = skills_loader + + @property + def name(self) -> str: + return "load_skill" + + @property + def description(self) -> str: + return "Load a skill by name. Returns the full SKILL.md content." + + @property + def parameters(self) -> dict[str, Any]: + return { + "type": "object", + "properties": {"name": {"type": "string", "description": "The skill name to load"}}, + "required": ["name"], + } + + async def execute(self, name: str, **kwargs: Any) -> str: + try: + content = self._skills_loader.load_skill(name) + if content is None: + return f"Error: Skill not found: {name}" + return content + except Exception as e: + return f"Error loading skill: {str(e)}"