mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-14 23:19:55 +00:00
Merge PR #2959: feat(skills): add disabled_skills config to exclude skills from loading
feat(skills): add disabled_skills config to exclude skills from loading
This commit is contained in:
commit
a81e4c1791
20
README.md
20
README.md
@ -1597,6 +1597,26 @@ When enabled, all incoming messages — regardless of which channel they arrive
|
||||
|
||||
> This is designed for single-user, multi-device setups. It is **off by default** — existing users see zero behavior change.
|
||||
|
||||
### Disabled Skills
|
||||
|
||||
nanobot ships with built-in skills, and your workspace can also define custom skills under `skills/`. If you want to hide specific skills from the agent, set `agents.defaults.disabledSkills` to a list of skill directory names:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"disabledSkills": ["github", "weather"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Disabled skills are excluded from the main agent's skill summary, from always-on skill injection, and from subagent skill summaries. This is useful when some bundled skills are unnecessary for your deployment or should not be exposed to end users.
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `agents.defaults.disabledSkills` | `[]` | List of skill directory names to exclude from loading. Applies to both built-in skills and workspace skills. |
|
||||
|
||||
## 🧩 Multiple Instances
|
||||
|
||||
Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint. Optionally pass `--workspace` during `onboard` when you want to initialize or update the saved workspace for a specific instance.
|
||||
|
||||
@ -22,11 +22,11 @@ class ContextBuilder:
|
||||
_MAX_RECENT_HISTORY = 50
|
||||
_RUNTIME_CONTEXT_END = "[/Runtime Context]"
|
||||
|
||||
def __init__(self, workspace: Path, timezone: str | None = None):
|
||||
def __init__(self, workspace: Path, timezone: str | None = None, disabled_skills: list[str] | None = None):
|
||||
self.workspace = workspace
|
||||
self.timezone = timezone
|
||||
self.memory = MemoryStore(workspace)
|
||||
self.skills = SkillsLoader(workspace)
|
||||
self.skills = SkillsLoader(workspace, disabled_skills=set(disabled_skills) if disabled_skills else None)
|
||||
|
||||
def build_system_prompt(
|
||||
self,
|
||||
|
||||
@ -152,6 +152,7 @@ class AgentLoop:
|
||||
session_ttl_minutes: int = 0,
|
||||
hooks: list[AgentHook] | None = None,
|
||||
unified_session: bool = False,
|
||||
disabled_skills: list[str] | None = None,
|
||||
):
|
||||
from nanobot.config.schema import ExecToolConfig, WebToolsConfig
|
||||
|
||||
@ -184,7 +185,7 @@ class AgentLoop:
|
||||
self._last_usage: dict[str, int] = {}
|
||||
self._extra_hooks: list[AgentHook] = hooks or []
|
||||
|
||||
self.context = ContextBuilder(workspace, timezone=timezone)
|
||||
self.context = ContextBuilder(workspace, timezone=timezone, disabled_skills=disabled_skills)
|
||||
self.sessions = session_manager or SessionManager(workspace)
|
||||
self.tools = ToolRegistry()
|
||||
self.runner = AgentRunner(provider)
|
||||
@ -197,6 +198,7 @@ class AgentLoop:
|
||||
max_tool_result_chars=self.max_tool_result_chars,
|
||||
exec_config=self.exec_config,
|
||||
restrict_to_workspace=restrict_to_workspace,
|
||||
disabled_skills=disabled_skills,
|
||||
)
|
||||
self._unified_session = unified_session
|
||||
self._running = False
|
||||
|
||||
@ -28,10 +28,11 @@ class SkillsLoader:
|
||||
specific tools or perform certain tasks.
|
||||
"""
|
||||
|
||||
def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None):
|
||||
def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None, disabled_skills: set[str] | None = None):
|
||||
self.workspace = workspace
|
||||
self.workspace_skills = workspace / "skills"
|
||||
self.builtin_skills = builtin_skills_dir or BUILTIN_SKILLS_DIR
|
||||
self.disabled_skills = disabled_skills or set()
|
||||
|
||||
def _skill_entries_from_dir(self, base: Path, source: str, *, skip_names: set[str] | None = None) -> list[dict[str, str]]:
|
||||
if not base.exists():
|
||||
@ -66,6 +67,9 @@ class SkillsLoader:
|
||||
self._skill_entries_from_dir(self.builtin_skills, "builtin", skip_names=workspace_names)
|
||||
)
|
||||
|
||||
if self.disabled_skills:
|
||||
skills = [s for s in skills if s["name"] not in self.disabled_skills]
|
||||
|
||||
if filter_unavailable:
|
||||
return [skill for skill in skills if self._check_requirements(self._get_skill_meta(skill["name"]))]
|
||||
return skills
|
||||
|
||||
@ -52,6 +52,7 @@ class SubagentManager:
|
||||
web_config: "WebToolsConfig | None" = None,
|
||||
exec_config: "ExecToolConfig | None" = None,
|
||||
restrict_to_workspace: bool = False,
|
||||
disabled_skills: list[str] | None = None,
|
||||
):
|
||||
from nanobot.config.schema import ExecToolConfig
|
||||
|
||||
@ -63,6 +64,7 @@ class SubagentManager:
|
||||
self.max_tool_result_chars = max_tool_result_chars
|
||||
self.exec_config = exec_config or ExecToolConfig()
|
||||
self.restrict_to_workspace = restrict_to_workspace
|
||||
self.disabled_skills = set(disabled_skills or [])
|
||||
self.runner = AgentRunner(provider)
|
||||
self._running_tasks: dict[str, asyncio.Task[None]] = {}
|
||||
self._session_tasks: dict[str, set[str]] = {} # session_key -> {task_id, ...}
|
||||
@ -236,7 +238,10 @@ class SubagentManager:
|
||||
from nanobot.agent.skills import SkillsLoader
|
||||
|
||||
time_ctx = ContextBuilder._build_runtime_context(None, None)
|
||||
skills_summary = SkillsLoader(self.workspace).build_skills_summary()
|
||||
skills_summary = SkillsLoader(
|
||||
self.workspace,
|
||||
disabled_skills=self.disabled_skills,
|
||||
).build_skills_summary()
|
||||
return render_template(
|
||||
"agent/subagent_system.md",
|
||||
time_ctx=time_ctx,
|
||||
|
||||
@ -591,6 +591,7 @@ def serve(
|
||||
channels_config=runtime_config.channels,
|
||||
timezone=runtime_config.agents.defaults.timezone,
|
||||
unified_session=runtime_config.agents.defaults.unified_session,
|
||||
disabled_skills=runtime_config.agents.defaults.disabled_skills,
|
||||
session_ttl_minutes=runtime_config.agents.defaults.session_ttl_minutes,
|
||||
)
|
||||
|
||||
@ -684,6 +685,7 @@ def gateway(
|
||||
channels_config=config.channels,
|
||||
timezone=config.agents.defaults.timezone,
|
||||
unified_session=config.agents.defaults.unified_session,
|
||||
disabled_skills=config.agents.defaults.disabled_skills,
|
||||
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
|
||||
)
|
||||
|
||||
@ -917,6 +919,7 @@ def agent(
|
||||
channels_config=config.channels,
|
||||
timezone=config.agents.defaults.timezone,
|
||||
unified_session=config.agents.defaults.unified_session,
|
||||
disabled_skills=config.agents.defaults.disabled_skills,
|
||||
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
|
||||
)
|
||||
restart_notice = consume_restart_notice_from_env()
|
||||
|
||||
@ -77,6 +77,7 @@ class AgentDefaults(Base):
|
||||
reasoning_effort: str | None = None # low / medium / high / adaptive - enables LLM thinking mode
|
||||
timezone: str = "UTC" # IANA timezone, e.g. "Asia/Shanghai", "America/New_York"
|
||||
unified_session: bool = False # Share one session across all channels (single-user multi-device)
|
||||
disabled_skills: list[str] = Field(default_factory=list) # Skill names to exclude from loading (e.g. ["summarize", "skill-creator"])
|
||||
session_ttl_minutes: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
|
||||
@ -82,6 +82,7 @@ class Nanobot:
|
||||
mcp_servers=config.tools.mcp_servers,
|
||||
timezone=defaults.timezone,
|
||||
unified_session=defaults.unified_session,
|
||||
disabled_skills=defaults.disabled_skills,
|
||||
session_ttl_minutes=defaults.session_ttl_minutes,
|
||||
)
|
||||
return cls(loop)
|
||||
|
||||
@ -250,3 +250,63 @@ def test_list_skills_openclaw_metadata_parsed_for_requirements(
|
||||
assert entries == [
|
||||
{"name": "openclaw_skill", "path": str(skill_path), "source": "workspace"},
|
||||
]
|
||||
|
||||
|
||||
def test_disabled_skills_excluded_from_list(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "ws"
|
||||
ws_skills = workspace / "skills"
|
||||
ws_skills.mkdir(parents=True)
|
||||
_write_skill(ws_skills, "alpha", body="# Alpha")
|
||||
beta_path = _write_skill(ws_skills, "beta", body="# Beta")
|
||||
builtin = tmp_path / "builtin"
|
||||
builtin.mkdir()
|
||||
|
||||
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills={"alpha"})
|
||||
entries = loader.list_skills(filter_unavailable=False)
|
||||
assert len(entries) == 1
|
||||
assert entries[0]["name"] == "beta"
|
||||
assert entries[0]["path"] == str(beta_path)
|
||||
|
||||
|
||||
def test_disabled_skills_empty_set_no_effect(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "ws"
|
||||
ws_skills = workspace / "skills"
|
||||
ws_skills.mkdir(parents=True)
|
||||
_write_skill(ws_skills, "alpha", body="# Alpha")
|
||||
_write_skill(ws_skills, "beta", body="# Beta")
|
||||
builtin = tmp_path / "builtin"
|
||||
builtin.mkdir()
|
||||
|
||||
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills=set())
|
||||
entries = loader.list_skills(filter_unavailable=False)
|
||||
assert len(entries) == 2
|
||||
|
||||
|
||||
def test_disabled_skills_excluded_from_build_skills_summary(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "ws"
|
||||
ws_skills = workspace / "skills"
|
||||
ws_skills.mkdir(parents=True)
|
||||
_write_skill(ws_skills, "alpha", body="# Alpha")
|
||||
_write_skill(ws_skills, "beta", body="# Beta")
|
||||
builtin = tmp_path / "builtin"
|
||||
builtin.mkdir()
|
||||
|
||||
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills={"alpha"})
|
||||
summary = loader.build_skills_summary()
|
||||
assert "alpha" not in summary
|
||||
assert "beta" in summary
|
||||
|
||||
|
||||
def test_disabled_skills_excluded_from_get_always_skills(tmp_path: Path) -> None:
|
||||
workspace = tmp_path / "ws"
|
||||
ws_skills = workspace / "skills"
|
||||
ws_skills.mkdir(parents=True)
|
||||
_write_skill(ws_skills, "alpha", metadata_json={"always": True}, body="# Alpha")
|
||||
_write_skill(ws_skills, "beta", metadata_json={"always": True}, body="# Beta")
|
||||
builtin = tmp_path / "builtin"
|
||||
builtin.mkdir()
|
||||
|
||||
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills={"alpha"})
|
||||
always = loader.get_always_skills()
|
||||
assert "alpha" not in always
|
||||
assert "beta" in always
|
||||
|
||||
@ -323,3 +323,27 @@ async def test_subagent_registers_grep_and_glob(tmp_path: Path) -> None:
|
||||
|
||||
assert "grep" in captured["tool_names"]
|
||||
assert "glob" in captured["tool_names"]
|
||||
|
||||
|
||||
def test_subagent_prompt_respects_disabled_skills(tmp_path: Path) -> None:
|
||||
bus = MessageBus()
|
||||
provider = MagicMock()
|
||||
provider.get_default_model.return_value = "test-model"
|
||||
skills_dir = tmp_path / "skills"
|
||||
(skills_dir / "alpha").mkdir(parents=True)
|
||||
(skills_dir / "alpha" / "SKILL.md").write_text("# Alpha\n\nhidden\n", encoding="utf-8")
|
||||
(skills_dir / "beta").mkdir(parents=True)
|
||||
(skills_dir / "beta" / "SKILL.md").write_text("# Beta\n\nshown\n", encoding="utf-8")
|
||||
|
||||
mgr = SubagentManager(
|
||||
provider=provider,
|
||||
workspace=tmp_path,
|
||||
bus=bus,
|
||||
max_tool_result_chars=4096,
|
||||
disabled_skills=["alpha"],
|
||||
)
|
||||
|
||||
prompt = mgr._build_subagent_prompt()
|
||||
|
||||
assert "alpha" not in prompt
|
||||
assert "beta" in prompt
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user