From cfa7c7b3369ed5387423bd25d2ce36b3c9e8d186 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <60681390+huanghuoguoguo@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:11:51 +0800 Subject: [PATCH] fix(tools): support skill mount root discovery --- .../pkg/provider/tools/loaders/native.py | 18 +++++++++ tests/unit_tests/provider/test_skill_tools.py | 39 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/langbot/pkg/provider/tools/loaders/native.py b/src/langbot/pkg/provider/tools/loaders/native.py index 6a4421a85..389be1d58 100644 --- a/src/langbot/pkg/provider/tools/loaders/native.py +++ b/src/langbot/pkg/provider/tools/loaders/native.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +import fnmatch import json import os @@ -908,6 +909,23 @@ else: path = str(parameters.get('path', '/workspace') or '/workspace') self.ap.logger.info(f'glob tool invoked: query_id={query.query_id} pattern={pattern} path={path}') + if path.rstrip('/') == skill_loader.SKILL_MOUNT_PREFIX: + skill_names = set(skill_loader.get_visible_skills(self.ap, query)) + skill_names.update(skill_loader.get_activated_skills(query)) + matches = [ + skill_loader.get_virtual_skill_mount_path(skill_name) + for skill_name in sorted(skill_names) + if fnmatch.fnmatchcase(skill_name, pattern) + ] + return { + 'ok': True, + 'matches': matches, + 'preview': '\n'.join(matches), + 'total': len(matches), + 'truncated': False, + 'truncated_by': None, + } + host_path, selected_skill = self._resolve_host_path( query, path, diff --git a/tests/unit_tests/provider/test_skill_tools.py b/tests/unit_tests/provider/test_skill_tools.py index bedfdbde8..9ef68031d 100644 --- a/tests/unit_tests/provider/test_skill_tools.py +++ b/tests/unit_tests/provider/test_skill_tools.py @@ -563,6 +563,45 @@ class TestSkillToolLoader: class TestNativeToolLoaderSkillPaths: + @pytest.mark.asyncio + async def test_glob_skill_root_lists_visible_and_activated_mounts(self): + from langbot.pkg.provider.tools.loaders.native import NativeToolLoader + from langbot.pkg.provider.tools.loaders.skill import PIPELINE_BOUND_SKILLS_KEY, register_activated_skill + + with tempfile.TemporaryDirectory() as tmpdir: + ap = _make_ap() + ap.box_service = SimpleNamespace(available=True, default_workspace=tmpdir) + ap.skill_mgr = SimpleNamespace( + skills={ + 'visible-skill': _make_skill_data(name='visible-skill', package_root=tmpdir), + 'hidden-skill': _make_skill_data(name='hidden-skill', package_root=tmpdir), + } + ) + loader = NativeToolLoader(ap) + query = SimpleNamespace( + query_id='q1', + variables={PIPELINE_BOUND_SKILLS_KEY: ['visible-skill']}, + ) + register_activated_skill(query, _make_skill_data(name='activated-skill', package_root=tmpdir)) + + result = await loader.invoke_tool( + 'glob', + {'path': '/workspace/.skills', 'pattern': '*'}, + query, + ) + + assert result == { + 'ok': True, + 'matches': [ + '/workspace/.skills/activated-skill', + '/workspace/.skills/visible-skill', + ], + 'preview': '/workspace/.skills/activated-skill\n/workspace/.skills/visible-skill', + 'total': 2, + 'truncated': False, + 'truncated_by': None, + } + @pytest.mark.asyncio async def test_read_visible_skill_file(self): from langbot.pkg.provider.tools.loaders.native import NativeToolLoader