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:
@@ -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}""")
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
Reference in New Issue
Block a user