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
This commit is contained in:
Ben
2026-03-15 07:18:20 +00:00
committed by Xubin Ren
parent c4628038c6
commit 45832ea499
5 changed files with 36 additions and 5 deletions

View File

@@ -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}""")

View File

@@ -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)."""

View File

@@ -118,7 +118,6 @@ class SkillsLoader:
lines = ["<skills>"]
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" <skill available=\"{str(available).lower()}\">")
lines.append(f" <name>{name}</name>")
lines.append(f" <description>{desc}</description>")
lines.append(f" <location>{path}</location>")
# Show missing requirements for unavailable skills
if not available:

View File

@@ -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)

View File

@@ -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)}"