From 4fc700b7e010c7d974afcc77f502fd66c22b318f Mon Sep 17 00:00:00 2001 From: Hyu Date: Wed, 30 Sep 2026 18:44:16 +0800 Subject: [PATCH] fix(tools): hide register_skill when Cloud sandboxes cannot scan host directories scan_skill_directory is disabled for Cloud-managed sandboxes, but the register_skill tool was still advertised to the model whenever a backend was available, so every call failed closed without a usable explanation. Advertise register_skill only when the runtime may scan sandbox directories, and keep activate available in both modes. --- .../provider/tools/loaders/skill_authoring.py | 33 ++++++++++++++----- tests/unit_tests/provider/test_skill_tools.py | 20 +++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py index 4a9b91459..92e6ea0a0 100644 --- a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py +++ b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py @@ -35,10 +35,7 @@ class SkillToolLoader(loader.ToolLoader): # Check if sandbox backend is available (same check as native tools) self._sandbox_available = await self._check_sandbox_available() if self._sandbox_available: - self._tools = [ - self._build_activate_skill_tool(), - self._build_register_skill_tool(), - ] + self._tools = self._build_skill_tools() else: self.ap.logger.info( 'Skill tools (activate/register_skill) are NOT available. ' @@ -54,14 +51,32 @@ class SkillToolLoader(loader.ToolLoader): if not await self._is_available(): return [] if not self._tools: - self._tools = [ - self._build_activate_skill_tool(), - self._build_register_skill_tool(), - ] + self._tools = self._build_skill_tools() return list(self._tools) + def _build_skill_tools(self) -> list[resource_tool.LLMTool]: + tools = [self._build_activate_skill_tool()] + if self._skill_registration_available(): + tools.append(self._build_register_skill_tool()) + else: + self.ap.logger.info( + 'register_skill is NOT available: Cloud sandboxes never scan arbitrary ' + 'host skill directories. Install skills through the skill store instead.' + ) + return tools + + def _skill_registration_available(self) -> bool: + """Host skill scanning is disabled for Cloud-managed sandboxes.""" + + box_service = getattr(self.ap, 'box_service', None) + return not bool(getattr(box_service, 'managed_admission_required', False)) + async def has_tool(self, name: str) -> bool: - return await self._is_available() and name in SKILL_TOOL_NAMES + if not await self._is_available() or name not in SKILL_TOOL_NAMES: + return False + if name == REGISTER_SKILL_TOOL_NAME: + return self._skill_registration_available() + return True async def _is_available(self) -> bool: """Check if skill tools should be available. diff --git a/tests/unit_tests/provider/test_skill_tools.py b/tests/unit_tests/provider/test_skill_tools.py index b0e7288ca..3ca95b272 100644 --- a/tests/unit_tests/provider/test_skill_tools.py +++ b/tests/unit_tests/provider/test_skill_tools.py @@ -737,3 +737,23 @@ async def test_native_skill_tools_require_runner_box_binding(): with pytest.raises(BoxValidationError, match='Runner must bind a Box'): await loader.invoke_tool('exec', {'command': 'true'}, _make_query()) ap.box_service.execute_tool.assert_not_awaited() + + + @pytest.mark.asyncio + async def test_register_skill_hidden_when_cloud_scanning_is_disabled(self): + from langbot.pkg.provider.tools.loaders.skill_authoring import SkillToolLoader + + ap = _make_ap() + ap.skill_mgr = _make_skill_manager({'demo': _make_skill_data(name='demo')}) + ap.box_service = SimpleNamespace( + available=True, + managed_admission_required=True, + get_backend_status=AsyncMock(return_value={'backend': {'available': True}}), + ) + + loader = SkillToolLoader(ap) + await loader.initialize() + + assert [tool.name for tool in await loader.get_tools()] == ['activate'] + assert await loader.has_tool('register_skill') is False + assert await loader.has_tool('activate') is True