Skip to content

Commit dd15978

Browse files
joaomdmouraclaude
andcommitted
fix(skills): resolve registry skills through the installed AMP client
Skill downloads built their own `PlusAPI` and authenticated it from `CREWAI_USER_PAT`, the platform integration token, or the saved CLI login. Managed runtimes have no user credential to offer: they install a client of their own, which `load_agent_from_repository` already resolves through, so Agent Repository lookups worked while the skill downloads beside them failed with 401. Skills now resolve their client the same way, via `resolve_plus_client()` next to the hook it reads. A client that can't fetch skills falls back to environment credentials and warns, so older runtimes behave as they do today. `resolve_plus_response()` shares the sync/async bridging both lookups need, since `PlusAPI` is synchronous while managed clients are not. Version pinning, which the same bug was hiding: - Registry refs accept `@org/name@version`, and `@org/name@v1.2.0` since people write it both ways. `parse_skill_ref()` returns a `SkillRef(org, name, version)`; `parse_registry_ref()` keeps its `(org, name)` shape and drops the pin, so existing callers are unaffected - Agent Repository agents record a version per skill, which was parsed off the response and dropped. Those pins now travel with the refs, so publishing a new version of a skill no longer changes every agent that uses it - A pinned ref only accepts a project-local copy declaring that version in its `metadata.version` frontmatter, and the cache reports a miss when the version it recorded differs — so a pin re-resolves rather than loading another version. Unpinned refs keep hitting the cache as before - An unknown pin fails instead of quietly falling back to the newest version Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c52d0d9 commit dd15978

11 files changed

Lines changed: 787 additions & 81 deletions

File tree

docs/edge/en/concepts/skills.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,32 @@ agent = Agent(
221221
)
222222
```
223223

224+
### Pin a Version
225+
226+
An unpinned reference resolves to the newest published version, so publishing a
227+
new version changes every agent that references it. Append `@<version>` to pin
228+
one instead:
229+
230+
```python
231+
agent = Agent(
232+
role="Senior Code Reviewer",
233+
goal="Review pull requests for quality and security issues",
234+
backstory="Staff engineer with expertise in secure coding practices.",
235+
skills=["@acme/code-review@1.2.0"], # pinned; a leading "v" also works
236+
)
237+
```
238+
239+
A pinned reference only accepts a cached or project-local copy that reports the
240+
same version in its `metadata.version` frontmatter, and re-downloads otherwise —
241+
a pin asks for a specific version rather than hinting at one. Pinning an
242+
unpublished version fails rather than falling back to the latest.
243+
244+
<Note>
245+
Agents from the **Agent Repository** are pinned automatically: the repository
246+
records a version alongside each skill it assigns, and the runtime applies those
247+
pins when it loads the agent.
248+
</Note>
249+
224250
### List
225251

226252
```shell Terminal

lib/crewai/src/crewai/experimental/skills/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
from crewai.skills import cache, events, registry
1313
from crewai.skills.cache import SkillCacheManager
1414
from crewai.skills.registry import (
15+
SkillRef,
1516
is_registry_ref,
1617
parse_registry_ref,
18+
parse_skill_ref,
1719
resolve_registry_ref,
1820
)
1921

@@ -26,10 +28,12 @@
2628

2729
__all__ = [
2830
"SkillCacheManager",
31+
"SkillRef",
2932
"cache",
3033
"events",
3134
"is_registry_ref",
3235
"parse_registry_ref",
36+
"parse_skill_ref",
3337
"registry",
3438
"resolve_registry_ref",
3539
]

lib/crewai/src/crewai/skills/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
from crewai.skills.models import Skill, SkillFrontmatter
1616
from crewai.skills.parser import SkillParseError
1717
from crewai.skills.registry import (
18+
SkillRef,
1819
is_registry_ref,
1920
parse_registry_ref,
21+
parse_skill_ref,
2022
resolve_registry_ref,
2123
)
2224

@@ -26,11 +28,13 @@
2628
"SkillCacheManager",
2729
"SkillFrontmatter",
2830
"SkillParseError",
31+
"SkillRef",
2932
"activate_skill",
3033
"discover_skills",
3134
"is_registry_ref",
3235
"load_skill",
3336
"load_skills",
3437
"parse_registry_ref",
38+
"parse_skill_ref",
3539
"resolve_registry_ref",
3640
]

lib/crewai/src/crewai/skills/cache.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from typing import TypedDict
1616
import zipfile
1717

18+
from crewai.skills.validation import versions_match
19+
1820

1921
_logger = logging.getLogger(__name__)
2022

@@ -39,13 +41,43 @@ def __init__(self, cache_root: Path | None = None) -> None:
3941
def _skill_dir(self, org: str, name: str) -> Path:
4042
return self._root / org / name
4143

42-
def get_cached_path(self, org: str, name: str) -> Path | None:
43-
"""Return the cached skill directory path if it exists, else None."""
44+
def get_cached_path(
45+
self, org: str, name: str, version: str | None = None
46+
) -> Path | None:
47+
"""Return the cached skill directory path if usable, else None.
48+
49+
Args:
50+
org: Organisation slug.
51+
name: Skill name.
52+
version: When given, the cached entry must record this version.
53+
The cache holds one version per skill, so a pinned lookup for a
54+
different version reports a miss and the caller re-downloads
55+
rather than loading the wrong version.
56+
57+
Returns:
58+
The cached skill directory, or None on a miss.
59+
"""
4460
skill_dir = self._skill_dir(org, name)
4561
meta_file = skill_dir / _META_FILENAME
46-
if skill_dir.is_dir() and meta_file.exists():
47-
return skill_dir
48-
return None
62+
if not (skill_dir.is_dir() and meta_file.exists()):
63+
return None
64+
if version is not None and not self._records_version(meta_file, version):
65+
return None
66+
return skill_dir
67+
68+
def _records_version(self, meta_file: Path, version: str) -> bool:
69+
"""Return True when the cache metadata records *version*."""
70+
try:
71+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
72+
except (OSError, json.JSONDecodeError):
73+
_logger.debug("Unreadable cache entry: %s", meta_file, exc_info=True)
74+
return False
75+
if not isinstance(meta, dict):
76+
_logger.debug("Malformed cache entry: %s", meta_file)
77+
return False
78+
# versions_match() treats a non-string version as no match, so a
79+
# corrupted entry reads as a miss rather than raising.
80+
return versions_match(version, meta.get("version"))
4981

5082
def store(
5183
self, org: str, name: str, version: str | None, archive_bytes: bytes

0 commit comments

Comments
 (0)