microsoft / microsoft/amplifier
[amplifier-bundle-skills] tool-skills: @namespace:path skill sources silently dropped when configured at mount time
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 3.1k
- Forks
- 261
- Avg merge
- 3h 28m
- Merged PRs (30d)
- 13
Description
Summary
The documented @bundle:skills source form works correctly when registered at runtime via load_skill(source="@bundle:skills"), but is silently dropped when supplied through the tools[tool-skills].config.skills: list at mount time. No error is raised; the entries simply do not register. This affects every bundle author who follows the documented "ship-your-own-skills" pattern.
In our case, 48 bundle-shipped skills failed to load into visibility for an unknown duration — the failure was only surfaced when a learning-digest hook flagged repeated load_skill failures referencing skills that exist on disk.
The root cause is two parallel resolution paths in tool-skills/__init__.py that handle @-prefixed sources inconsistently — runtime path dispatches to mention_resolver, mount-time path falls through to Path().
Reproducer
Minimal tools config in any behavior file:
tools:
- module: tool-skills
source: git+https://github.com/microsoft/amplifier-bundle-skills@main#subdirectory=modules/tool-skills
config:
skills:
- "@mybundle:skills" # <-- silently dropped at mount; never resolved
Expected: skills under <mybundle root>/skills/ are discovered and visible.
Actual: zero skills from the bundle load. No error, no warning at default log level — only a logger.debug line "Local skill source does not exist: /cwd/@mybundle:skills".
To prove @namespace: is otherwise functional, the same source resolves correctly via the runtime path:
load_skill(source="@mybundle:skills") # works — discovers and registers all skills
Expected vs. actual behavior
The README and skills-instructions.md both document @mybundle:skills as a canonical config source type:
Source type Example When to use Bundle reference @mybundle:skillsSkills shipped inside your own bundle
Users following this documentation will find their skills missing with no diagnostic. The workaround (use the git+ssh://...#subdirectory=skills form, or an absolute local path) requires reading source code to discover.
Root cause
In modules/tool-skills/amplifier_module_tool_skills/__init__.py:
Mount-time path — _resolve_skill_sources (lines ~57–134):
# Check if any sources are remote (need async resolution)
has_remote = any(is_remote_source(s) for s in sources)
if has_remote:
return await resolve_skill_sources(sources)
else:
# All local - just expand paths
resolved = []
for source in sources:
path = Path(source).expanduser().resolve()
if path.exists():
resolved.append(path)
else:
logger.debug(f"Local skill source does not exist: {path}")
return resolved if resolved else get_default_skills_dirs()
is_remote_source (in sources.py:24) matches only git+… / https:// / http:// prefixes, so @mybundle:skills returns False. The source then falls into the local branch where Path("@mybundle:skills").expanduser().resolve() produces <cwd>/@mybundle:skills, which does not exist, and the source is dropped at debug log level.
Runtime path — SkillsTool._resolve_source (lines ~488–514):
async def _resolve_source(self, source: str) -> Path | None:
# @namespace:path — use mention resolver
if source.startswith("@"):
if self.coordinator:
resolver = self.coordinator.get_capability("mention_resolver")
if resolver:
return resolver.resolve(source)
return None
if is_remote_source(source):
return await resolve_skill_source(source)
path = Path(source).expanduser().resolve()
return path if path.exists() else None
This method correctly dispatches @-prefixed sources via mention_resolver — but it is only invoked from execute() at runtime (line ~530), never from _resolve_skill_sources at mount.
The two paths diverged: the runtime resolver was extended to support @namespace: (likely when mention-resolution capability was added to the kernel), but the equivalent change was never applied to the mount-time resolver.
Test coverage confirms the omission: tests/test_source_parameter.py:110-119 exercises the runtime path only; no test exercises mount-time resolution of @-prefixed sources.
Proposed fix
Pre-resolve @-prefixed sources via mention_resolver before the is_remote_source dispatch. Minimal patch to _resolve_skill_sources:
# Insert immediately after sources are collected (before "has_remote = any(...)"):
# Pre-resolve @namespace: sources via mention_resolver (parallel to runtime path)
resolver = coordinator.get_capability("mention_resolver") if coordinator else None
resolved_sources: list[str] = []
for source in sources:
if isinstance(source, str) and source.startswith("@"):
if resolver is None:
logger.warning(
"Cannot resolve @-namespace skill source %r — "
"mention_resolver capability not available", source,
)
continue
resolved_path = resolver.resolve(source)
if resolved_path is None:
logger.warning(
"Could not resolve @-namespace skill source %r — "
"no matching bundle registered", source,
)
continue
resolved_sources.append(str(resolved_path))
else:
resolved_sources.append(source)
sources = resolved_sources
Notes:
- Uses
logger.warningrather thanlogger.debugfor@-source failures, because a configured@-source that fails to resolve is almost certainly a misconfiguration the user wants to know about (in contrast to a genuinely-optional local path that may not exist on this machine). - The resolved string still flows through the existing
has_remote/ local-Path branches, so cache reuse and async resolution behavior are unchanged for the non-@paths. - A longer-term refactor would extract the per-source dispatch into a shared helper called by both
_resolve_skill_sources(mount) and_resolve_source(runtime), eliminating the parallel-path drift class of bug. Happy to submit either form as a PR if you'd like.
Suggested test addition
async def test_resolve_skill_sources_handles_namespace_mention(
monkeypatch, tmp_path,
):
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
class FakeResolver:
def resolve(self, mention: str):
assert mention == "@mybundle:skills"
return skills_dir
class FakeCoordinator:
config = {}
def get_capability(self, name):
return FakeResolver() if name == "mention_resolver" else None
config = {"skills": ["@mybundle:skills"]}
result = await _resolve_skill_sources(config, FakeCoordinator())
assert skills_dir in result
Workarounds for affected users
Either of these works today without an upstream fix:
-
Git URL form (preferred, portable):
skills: - "git+ssh://git@github.com/<owner>/<bundle>@main#subdirectory=skills"Hits
is_remote_source→ cached resolution. Same source URL the bundle itself loads from, so the cache is reused. This is the workaround we shipped on our side ([commit reference]). -
Absolute local path (single-machine):
skills: - /absolute/path/to/bundle/skillsLoses cross-machine portability.
Neither workaround is discoverable from the documentation; both required reading tool-skills source.
Severity / impact
- Severity: medium. Silent failure with no error surface is the worst-case UX class. The fact that documented behavior diverges from actual behavior means anyone following the canonical README example hits this and has no diagnostic to start from.
- Generality: affects every bundle author who ships skills inside their own bundle and uses the documented
@bundle:skillspattern. The pattern is documented and encouraged but the third-party-bundle-shipping-its-own-skills ecosystem is still young, so practical impact today is narrow but will grow. - Fix size: ~12 lines plus a regression test.
Related
- Documentation showing
@mybundle:skillsas canonical:bundle.mdin this repo (lines ~53-64) andcontext/skills-instructions.md. - Test coverage gap:
tests/test_source_parameter.py:110-119(runtime-only).
Happy to convert this into a PR if it's useful — let me know which form you'd prefer for the fix (the minimal patch above, or the longer-term shared-helper refactor).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read modules/tool-skills/amplifier_module_tool_skills/init.py and sources.py, comparing _resolve_skill_sources with SkillsTool._resolve_source and the mention_resolver capability. Run tests/test_source_parameter.py and inspect existing coordinator fixtures. Done means mount-time @namespace:path sources resolve and register skills, with a regression test covering the behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100