Skip to content

feat: Skills Repository — registry, cache, CLI, and SDK integration#5867

Open
alex-clawd wants to merge 7 commits into
mainfrom
alex/skills-repository
Open

feat: Skills Repository — registry, cache, CLI, and SDK integration#5867
alex-clawd wants to merge 7 commits into
mainfrom
alex/skills-repository

Conversation

@alex-clawd
Copy link
Copy Markdown
Contributor

@alex-clawd alex-clawd commented May 19, 2026

What

Adds a Skills Repository feature: users can publish, install, and use skills from the CrewAI registry using @org/skill-name refs.

Why

Skills (SKILL.md instruction bundles) currently only work as local filesystem paths. This adds a distribution layer so teams can:

  • Share skills across projects via crewai skill publish
  • Install skills with crewai skill install @org/skill-name
  • Reference registry skills directly: Agent(skills=['@acme/compliance-checker'])
  • Get prompted to download missing skills on first run

How It Works

Resolution order (local-first):

  1. ./skills/{name}/ — project-local (editable, committed to repo)
  2. ~/.crewai/skills/{org}/{name}/ — global cache
  3. Registry download via PlusAPI (with interactive consent prompt)

CI/headless environments: CI=1 or CREWAI_NONINTERACTIVE=1 → no prompt, raises SkillNotCachedError with install instructions.

Components

Area Files What
Cache skills/cache.py SkillCacheManager~/.crewai/skills/ with .crewai_meta.json, path-traversal-safe extraction
Registry skills/registry.py @org/skill ref parsing, resolution, interactive download prompt
Models skills/models.py version field on SkillFrontmatter (optional, backward compat)
Events skill_events.py SkillDownloadStartedEvent, SkillDownloadCompletedEvent
SDK agent/core.py, base_agent.py, crew.py Skills type widened to str, set_skills() resolves registry refs
API plus_api.py get_skill(), publish_skill(), list_skills()
CLI cli/skills/main.py, cli.py crewai skill create/install/publish/list
Scaffolding templates/crew/skills/, templates/flow/skills/ New projects get a skills/ directory

Usage

# Registry skill
Agent(role='Analyst', skills=['@acme/compliance-checker'])

# YAML
agents:
  analyst:
    skills:
      - '@acme/compliance-checker'
      - ./skills/custom-process
crewai skill create my-skill          # scaffold (context-aware)
crewai skill install @acme/my-skill   # download to ./skills/ or cache
crewai skill publish --org acme       # upload to registry
crewai skill list                     # show installed

Tests

  • 91 SDK skill tests (cache, registry, models)
  • 15 CLI skill tests (create, install, publish, list)
  • All existing skill tests continue to pass

Summary by CodeRabbit

  • New Features

    • New crewai skill CLI for create, install, publish, and list workflows (project install, visibility, org, force options)
    • Agents and crews accept registry-style skill references (@org/name) with automatic resolution and precedence for project-local skills
    • Global filesystem skill cache with install, listing, and invalidate behavior; publish validates frontmatter and returns a status URL
  • Events

    • Skill download lifecycle events (start/completed)
  • Tests

    • Comprehensive tests for CLI, cache, registry resolution, and frontmatter version handling

Review Change Stack

Adds a Skills Repository feature allowing users to publish, install,
and use skills from the CrewAI registry with @org/skill-name refs.

## What's New

### SDK (lib/crewai/)
- SkillFrontmatter: added optional 'version' field (backward compatible)
- SkillCacheManager: manages ~/.crewai/skills/{org}/{name}/ with
  .crewai_meta.json tracking, path-traversal-safe tar extraction
- SkillRegistry: parse @org/skill-name refs, local-first resolution
  (./skills/ > cache > download), interactive prompt on first use,
  CI-mode guard (CREWAI_NONINTERACTIVE/CI env vars)
- Agent.skills and Crew.skills widened to accept str refs (@org/name)
- set_skills() resolves registry refs with org-prefixed dedup keys
- New events: SkillDownloadStartedEvent, SkillDownloadCompletedEvent

### CLI (lib/cli/)
- crewai skill create <name> — context-aware (project vs standalone)
- crewai skill install @org/name — downloads to ./skills/ or cache
- crewai skill publish — ZIP + upload to org registry
- crewai skill list — show installed skills

### PlusAPI (lib/crewai-core/)
- Added SKILLS_RESOURCE, get_skill(), publish_skill(), list_skills()

### Scaffolding
- crew and flow templates now include skills/ directory

### Tests
- 91 SDK skill tests + 15 CLI skill tests, all passing
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements a skill registry: parse/resolve @org/name refs, a local/global cache with safe extraction and metadata, PlusAPI skill endpoints, agent/crew schema validators to accept registry refs, CLI crewai skill commands (create/install/publish/list), and tests covering registry, cache, CLI, and models.

Changes

CrewAI Agent Skills Registry

Layer / File(s) Summary
Registry utilities and tests
lib/crewai/src/crewai/skills/registry.py, lib/crewai/tests/skills/test_registry.py
Detects and parses @org/name refs, exposes SkillNotCachedError, non-interactive detection, and tests for parsing, errors, and resolution order.
Skill cache storage and tests
lib/crewai/src/crewai/skills/cache.py, lib/crewai/tests/skills/test_cache.py
SkillCacheManager stores skills under ~/.crewai/skills/{org}/{name}/, safely extracts archives with traversal protection, writes .crewai_meta.json, lists and invalidates cache entries, and is covered by tests.
Plus API skill endpoints
lib/crewai-core/src/crewai_core/plus_api.py
Adds SKILLS_RESOURCE and methods: get_skill, publish_skill, and list_skills.
Skill models, events, and exports
lib/crewai/src/crewai/skills/models.py, lib/crewai/src/crewai/events/types/skill_events.py, lib/crewai/src/crewai/skills/__init__.py, lib/crewai/tests/skills/test_models_version.py
Adds optional version to SkillFrontmatter, introduces SkillDownloadStartedEvent/SkillDownloadCompletedEvent, exports SkillCacheManager, and tests frontmatter version behavior.
Agent and Crew integration
lib/crewai/src/crewai/agents/agent_builder/base_agent.py, lib/crewai/src/crewai/crew.py, lib/crewai/src/crewai/agent/core.py
Widen skills fields to accept str refs; add validators to coerce non-@ strings to Path and update Agent.set_skills() to resolve registry refs and deduplicate resolved skills.
CLI skill group and SkillCommand
lib/cli/src/crewai_cli/cli.py, lib/cli/src/crewai_cli/skills/main.py, lib/cli/tests/skills/test_main.py
Registers crewai skill Click group and subcommands; implements SkillCommand for create/install/publish/list with archive packing/unpacking, frontmatter parsing, PlusAPI interaction, traversal-safe extraction, and Rich table listing; includes CLI tests for success and error scenarios.

Sequence Diagram

sequenceDiagram
  participant User as User
  participant SkillCLI as crewai skill (Click)
  participant SkillCommand as SkillCommand
  participant PlusAPI as PlusAPI
  participant Cache as SkillCacheManager
  participant Agent as Agent

  User->>SkillCLI: run create/install/publish/list
  SkillCLI->>SkillCommand: dispatch command

  rect rgba(100, 150, 200, 0.5)
  Note over SkillCommand: create -> scaffold directories, write SKILL.md
  SkillCommand->>SkillCommand: create SKILL.md and assets
  end

  rect rgba(150, 100, 200, 0.5)
  Note over SkillCommand: install -> fetch & cache
  SkillCommand->>PlusAPI: get_skill(org, name)
  PlusAPI-->>SkillCommand: base64 archive response
  SkillCommand->>Cache: store(org, name, version, archive_bytes)
  Cache-->>SkillCommand: cached_path
  end

  rect rgba(200, 150, 100, 0.5)
  Note over SkillCommand: publish -> build ZIP and upload
  SkillCommand->>SkillCommand: parse SKILL.md frontmatter
  SkillCommand->>SkillCommand: build in-memory ZIP
  SkillCommand->>PlusAPI: publish_skill(org, name, version, encoded_file)
  PlusAPI-->>SkillCommand: publish response (status URL)
  end

  rect rgba(100, 200, 150, 0.5)
  Note over SkillCommand: list -> aggregate installed skills
  SkillCommand->>Cache: list_cached()
  Cache-->>SkillCommand: [SkillMetadata...]
  SkillCommand->>User: display Rich table
  end

  rect rgba(120, 120, 180, 0.5)
  Note over Agent: runtime resolution of registry refs
  Agent->>SkillCommand: resolve_registry_ref(`@org/name`)
  SkillCommand->>Cache: get_cached_path(org, name)
  alt cache miss & interactive
    SkillCommand->>PlusAPI: get_skill(...)
    PlusAPI-->>SkillCommand: archive
    SkillCommand->>Cache: store(...)
  end
  SkillCommand-->>Agent: activated Skill
  end
Loading

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers:

  • greysonlalonde

🐰 A little rabbit builds a skill-filled glade,
With caches, crates, and YAML laid,
Publish, install, and list with cheer,
Agents fetch the skills they hold dear,
Hopping bytes and metadata near!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a Skills Repository feature with registry, cache, CLI, and SDK integration capabilities across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alex/skills-repository

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread lib/cli/src/crewai_cli/skills/main.py Fixed
Comment thread lib/crewai/tests/skills/test_cache.py Fixed
Comment thread lib/cli/tests/skills/test_main.py Fixed
Comment thread lib/crewai/src/crewai/skills/cache.py Fixed
Comment thread lib/cli/src/crewai_cli/skills/main.py Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
lib/crewai/tests/skills/test_cache.py (1)

16-37: 💤 Low value

Helper function contains redundant archive creation.

The _make_tar_gz function creates the archive twice (lines 18-27 are unused, then lines 30-37 create the actual output). The first block can be removed.

Proposed simplification
 def _make_tar_gz(files: dict[str, str]) -> bytes:
     """Build an in-memory .tar.gz containing the given filename → content mapping."""
-    buf = io.BytesIO()
-    with gzip.GzipFile(fileobj=buf, mode="wb") as gz:
-        gz_buf = io.BytesIO()
-        with tarfile.open(fileobj=gz_buf, mode="w") as tf:
-            for name, content in files.items():
-                data = content.encode()
-                info = tarfile.TarInfo(name=name)
-                info.size = len(data)
-                tf.addfile(info, io.BytesIO(data))
-        gz.write(gz_buf.getvalue())
-    buf.seek(0)
-    # Re-create properly: gzip wrapping a tar stream
     out = io.BytesIO()
     with tarfile.open(fileobj=out, mode="w:gz") as tf:
         for name, content in files.items():
             data = content.encode()
             info = tarfile.TarInfo(name=name)
             info.size = len(data)
             tf.addfile(info, io.BytesIO(data))
     return out.getvalue()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/skills/test_cache.py` around lines 16 - 37, The helper
_make_tar_gz currently builds the tar.gz archive twice: the first in-memory
gzip/tar creation (using buf, gz, gz_buf and tf) is redundant and unused; remove
that entire first block (everything involving buf, gzip.GzipFile, gz_buf and the
inner tarfile loop) and keep the final tarfile.open(fileobj=out, mode="w:gz")
section that writes the archive once and returns out.getvalue(), ensuring the
function still accepts files: dict[str, str], encodes content to bytes, sets
TarInfo.size, and adds files via tf.addfile as in the remaining code.
lib/crewai-core/src/crewai_core/plus_api.py (1)

242-259: ⚡ Quick win

Consider defining a TypedDict for the publish payload.

For consistency with the existing PublishToolPayload pattern and better type safety, consider defining a PublishSkillPayload TypedDict:

class PublishSkillPayload(TypedDict):
    org: str
    name: str
    version: str
    public: bool
    description: str | None
    file: str

Then use it in the method signature:

def publish_skill(...) -> httpx.Response:
    payload: PublishSkillPayload = {
        "org": org,
        ...
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai-core/src/crewai_core/plus_api.py` around lines 242 - 259, Add a
PublishSkillPayload TypedDict mirroring the existing PublishToolPayload pattern
(fields: org, name, version, public, description, file) and annotate the local
payload in publish_skill with that type; update the publish_skill function to
declare payload: PublishSkillPayload = {...} so the payload construction uses
the new TypedDict for stronger typing and consistency with other publish
methods.
lib/crewai/src/crewai/skills/models.py (1)

81-84: ⚡ Quick win

Consider adding semantic version validation.

The description mentions "Semantic version of the skill, e.g. '1.0.0'", but there's no pattern validation. The name field has pattern validation via SKILL_NAME_PATTERN. Consider whether version should validate against a semver pattern for consistency:

SEMVER_PATTERN = r"^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$"

version: str | None = Field(
    default=None,
    pattern=SEMVER_PATTERN,
    description="Semantic version of the skill, e.g. '1.0.0'. Optional for local skills.",
)

If flexible versioning is intentional, this can be skipped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/skills/models.py` around lines 81 - 84, The version
Field in models.py lacks semver validation despite its description; add a
SEMVER_PATTERN constant (e.g. matching major.minor.patch with optional
pre-release/build) and apply it to the existing version: str | None = Field(...)
by adding pattern=SEMVER_PATTERN so the version value is validated; update
imports if needed and ensure the symbol names SEMVER_PATTERN and the version
Field are used exactly as in the diff.
lib/cli/tests/skills/test_main.py (1)

83-120: ⚡ Quick win

Add a traversal regression test for install archive extraction.

Please add a test archive containing entries like ../pwned.txt (and absolute paths) and assert install() rejects it. This protects the extraction boundary from future regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/tests/skills/test_main.py` around lines 83 - 120, Extend
TestSkillInstall to add a traversal-regression test: modify or add a test (e.g.,
alongside test_install_in_project) that uses _zip_skill (or a new helper similar
to _zip_skill) to produce a ZIP containing traversal entries like "../pwned.txt"
and an absolute path (e.g., "/tmp/pwned.txt"), return it as a
data:application/zip;base64,... mock response as in test_install_in_project,
then call skill_command.install("`@acme/my-skill`") and assert the installer
rejects the archive (e.g., raises SystemExit) and that no files outside the
intended skills/my-skill directory are created; reference
TestSkillInstall._zip_skill and skill_command.install when locating where to add
the new test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/cli/src/crewai_cli/skills/main.py`:
- Around line 282-301: The archive extraction in _unpack_archive (and its TAR
helper _safe_extractall and the ZIP branch using ZipFile.extractall) is
vulnerable to path-traversal; replace the unvalidated extractall calls with
per-member path checks that resolve each member's destination path and ensure it
is inside dest using Path.resolve() and Path.relative_to(dest.resolve()) (raise
an error or skip on failure). For TAR: iterate tf.getmembers(), compute the
target path for each member (join dest with member.name), ensure
target.relative_to(dest.resolve()) succeeds, then use tf.extractfile or
tf.extract for safe extraction (preserving file types securely). For ZIP:
iterate zf.infolist(), compute each member's target path, validate with
Path.relative_to(dest.resolve()) and write files only after validation. Remove
any string-prefix startswith checks and use Path.relative_to() for ancestry
validation. Ensure symlinks or special files are handled safely (skip or
sanitize) and surface an error if any member fails validation.

In `@lib/crewai/src/crewai/skills/cache.py`:
- Around line 113-120: The path-traversal check in _safe_extractall is unsafe
because using str(...).startswith(dest_resolved) can be bypassed; update the
check to ensure each resolved member_path is actually inside dest_resolved by
using a proper path containment test (e.g., Path.is_relative_to(dest_resolved)
if available or using member_path.relative_to(dest_resolved) in a try/except to
detect escapes) before calling tf.extractall(dest), and raise the same
ValueError when the member is outside; keep the same function name
_safe_extractall and the tf.extractall call but replace the string-prefix test
with the robust containment test.

In `@lib/crewai/src/crewai/skills/registry.py`:
- Around line 16-24: The exception message in SkillNotCachedError.__init__ uses
a literal "{ref}" instead of interpolating the ref value; update the constructor
so the install hint includes the actual ref by making the second string an
f-string (or otherwise formatting it) so the combined message contains the real
ref for the user guidance in SkillNotCachedError.
- Around line 98-112: The bare "except: pass" blocks around the local and cached
skill loads (wrapping load_skill_metadata and activate_skill) hide real errors;
replace them with a narrowed exception handler or capture the exception as "e"
and log it before falling through. Specifically, in the try/except around
load_skill_metadata(local_path)/activate_skill(...) and the try/except around
load_skill_metadata(cached_path)/activate_skill(...), change "except Exception:
pass" to either "except (FileNotFoundError, PermissionError, ValueError,
ImportError) as e:" or "except Exception as e:" and call a logger (e.g.
logging.exception or an existing module logger) to record the error and context
(org/name/local_path or cached_path and SKILL.md) so failures are visible while
still allowing the code to proceed to the download branch.

In `@lib/crewai/tests/skills/test_models_version.py`:
- Around line 28-31: The test test_frontmatter_is_frozen should assert the
specific Pydantic error type instead of catching a broad Exception: change the
pytest.raises(Exception) to pytest.raises(ValidationError) and import
ValidationError from pydantic, ensuring the assertion uses the SkillFrontmatter
instance to trigger immutability (fm.version = "2.0.0") and that the failure is
specifically matched to ValidationError raised by the frozen model.

---

Nitpick comments:
In `@lib/cli/tests/skills/test_main.py`:
- Around line 83-120: Extend TestSkillInstall to add a traversal-regression
test: modify or add a test (e.g., alongside test_install_in_project) that uses
_zip_skill (or a new helper similar to _zip_skill) to produce a ZIP containing
traversal entries like "../pwned.txt" and an absolute path (e.g.,
"/tmp/pwned.txt"), return it as a data:application/zip;base64,... mock response
as in test_install_in_project, then call skill_command.install("`@acme/my-skill`")
and assert the installer rejects the archive (e.g., raises SystemExit) and that
no files outside the intended skills/my-skill directory are created; reference
TestSkillInstall._zip_skill and skill_command.install when locating where to add
the new test.

In `@lib/crewai-core/src/crewai_core/plus_api.py`:
- Around line 242-259: Add a PublishSkillPayload TypedDict mirroring the
existing PublishToolPayload pattern (fields: org, name, version, public,
description, file) and annotate the local payload in publish_skill with that
type; update the publish_skill function to declare payload: PublishSkillPayload
= {...} so the payload construction uses the new TypedDict for stronger typing
and consistency with other publish methods.

In `@lib/crewai/src/crewai/skills/models.py`:
- Around line 81-84: The version Field in models.py lacks semver validation
despite its description; add a SEMVER_PATTERN constant (e.g. matching
major.minor.patch with optional pre-release/build) and apply it to the existing
version: str | None = Field(...) by adding pattern=SEMVER_PATTERN so the version
value is validated; update imports if needed and ensure the symbol names
SEMVER_PATTERN and the version Field are used exactly as in the diff.

In `@lib/crewai/tests/skills/test_cache.py`:
- Around line 16-37: The helper _make_tar_gz currently builds the tar.gz archive
twice: the first in-memory gzip/tar creation (using buf, gz, gz_buf and tf) is
redundant and unused; remove that entire first block (everything involving buf,
gzip.GzipFile, gz_buf and the inner tarfile loop) and keep the final
tarfile.open(fileobj=out, mode="w:gz") section that writes the archive once and
returns out.getvalue(), ensuring the function still accepts files: dict[str,
str], encodes content to bytes, sets TarInfo.size, and adds files via tf.addfile
as in the remaining code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 524644bf-d2c5-4b17-bb66-5abd76eb2070

📥 Commits

Reviewing files that changed from the base of the PR and between 35f693c and 9965d67.

📒 Files selected for processing (19)
  • lib/cli/src/crewai_cli/cli.py
  • lib/cli/src/crewai_cli/skills/__init__.py
  • lib/cli/src/crewai_cli/skills/main.py
  • lib/cli/src/crewai_cli/templates/crew/skills/.gitkeep
  • lib/cli/src/crewai_cli/templates/flow/skills/.gitkeep
  • lib/cli/tests/skills/__init__.py
  • lib/cli/tests/skills/test_main.py
  • lib/crewai-core/src/crewai_core/plus_api.py
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai/src/crewai/agents/agent_builder/base_agent.py
  • lib/crewai/src/crewai/crew.py
  • lib/crewai/src/crewai/events/types/skill_events.py
  • lib/crewai/src/crewai/skills/__init__.py
  • lib/crewai/src/crewai/skills/cache.py
  • lib/crewai/src/crewai/skills/models.py
  • lib/crewai/src/crewai/skills/registry.py
  • lib/crewai/tests/skills/test_cache.py
  • lib/crewai/tests/skills/test_models_version.py
  • lib/crewai/tests/skills/test_registry.py

Comment thread lib/cli/src/crewai_cli/skills/main.py
Comment thread lib/crewai/src/crewai/skills/cache.py
Comment thread lib/crewai/src/crewai/skills/registry.py
Comment thread lib/crewai/src/crewai/skills/registry.py Outdated
Comment thread lib/crewai/tests/skills/test_models_version.py
Lint:
- Remove unused imports (click, pytest, json)
- Replace try-except-pass with logging (S110)
- Fix unprotected zipfile.extractall (S202)

Security:
- Path traversal: startswith → is_relative_to for tar extraction
- Add path traversal protection to ZIP extraction via _safe_extract_zip
- Both cache.py and CLI main.py hardened

Type checker:
- Fix import path: crewai.events.event_bus (not crewai_event_bus)
- Remove unused type: ignore comments
- Fix type mismatches in set_skills() variable types

Code quality:
- Fix f-string interpolation in SkillNotCachedError
- Use ValidationError instead of Exception in test
@alex-clawd
Copy link
Copy Markdown
Contributor Author

All 10 CodeRabbit comments addressed in commit 3e0372d:

  • ✅ Unused imports removed (click, pytest, json)
  • ✅ try-except-pass → logging in registry.py, cache.py, CLI main.py
  • ✅ Path traversal: startswith → is_relative_to() for both tar and zip extraction
  • ✅ ZIP extraction hardened with _safe_extract_zip() helper
  • ✅ f-string interpolation fixed in SkillNotCachedError
  • ✅ Import path fixed: crewai.events.event_bus
  • ✅ Type mismatches resolved in set_skills()
  • ✅ Test uses ValidationError instead of bare Exception

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
lib/crewai/tests/skills/test_cache.py (1)

38-117: ⚡ Quick win

Add traversal-regression tests for malicious archive entries.

Current coverage is mostly happy-path. Please add explicit cases for ../ and absolute-path entries (and ZIP variants if store() supports ZIP) to assert extraction is rejected and nothing is written outside cache_root. This guards the recent path-traversal hardening from regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/skills/test_cache.py` around lines 38 - 117, Tests currently
lack cases ensuring SkillCacheManager.store is safe against path-traversal in
archives; add unit tests that create malicious archives (tar.gz entries with
"../" paths and absolute paths, and ZIP equivalents if SkillCacheManager.store
accepts ZIP) and call SkillCacheManager.store(...) to assert the store call
either rejects the archive (raises an exception) or sanitizes it by not writing
any files outside the provided cache_root and not creating unexpected files; use
get_cached_path(...) and inspect the tmp_path parent and siblings to assert
nothing was written outside cache_root, and also verify that a successful store
does not include files with traversal paths and that metadata is handled
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/crewai/tests/skills/test_cache.py`:
- Around line 38-117: Tests currently lack cases ensuring
SkillCacheManager.store is safe against path-traversal in archives; add unit
tests that create malicious archives (tar.gz entries with "../" paths and
absolute paths, and ZIP equivalents if SkillCacheManager.store accepts ZIP) and
call SkillCacheManager.store(...) to assert the store call either rejects the
archive (raises an exception) or sanitizes it by not writing any files outside
the provided cache_root and not creating unexpected files; use
get_cached_path(...) and inspect the tmp_path parent and siblings to assert
nothing was written outside cache_root, and also verify that a successful store
does not include files with traversal paths and that metadata is handled
consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75dd2f6e-e6b0-4908-a6a5-d1114e4ff08f

📥 Commits

Reviewing files that changed from the base of the PR and between 9965d67 and 3e0372d.

📒 Files selected for processing (7)
  • lib/cli/src/crewai_cli/skills/main.py
  • lib/cli/tests/skills/test_main.py
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai/src/crewai/skills/cache.py
  • lib/crewai/src/crewai/skills/registry.py
  • lib/crewai/tests/skills/test_cache.py
  • lib/crewai/tests/skills/test_models_version.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • lib/crewai/tests/skills/test_models_version.py
  • lib/crewai/src/crewai/skills/cache.py
  • lib/cli/src/crewai_cli/skills/main.py
  • lib/crewai/src/crewai/skills/registry.py
  • lib/crewai/src/crewai/agent/core.py
  • lib/cli/tests/skills/test_main.py

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
lib/cli/src/crewai_cli/skills/main.py (2)

139-141: 💤 Low value

Remove unnecessary import — plus_api_client is already used at line 110.

This import with as _ is dead code. self.plus_api_client.get_skill() is called at line 110 before the if in_project: branch, so if crewai_core wasn't available, the method would fail there. The comment "ensure crewai_core present" is misleading since that check happens implicitly earlier.

🧹 Suggested cleanup
         else:
-            from crewai_core.plus_api import (
-                PlusAPI as _,  # noqa: F401 (ensure crewai_core present)
-            )
-
             cache_dir = Path.home() / ".crewai" / "skills" / org / name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/src/crewai_cli/skills/main.py` around lines 139 - 141, Remove the
dead-import that tries to "ensure crewai_core present" — delete the import block
"from crewai_core.plus_api import PlusAPI as _  # noqa: F401" in skills/main.py
because plus_api_client is already used earlier
(self.plus_api_client.get_skill()) and the import is redundant and misleading;
simply remove that import line and its comment so the code does not contain a
no-op alias.

147-147: 💤 Low value

Remove redundant import — json is already imported at line 7.

Re-importing json as _json is unnecessary since json is already available at module scope. Use the existing import.

🧹 Suggested fix
             from datetime import datetime, timezone
-            import json as _json

             meta = {
                 "org": org,
                 "name": name,
                 "version": version,
                 "installed_at": datetime.now(tz=timezone.utc).isoformat(),
             }
-            (cache_dir / ".crewai_meta.json").write_text(_json.dumps(meta, indent=2))
+            (cache_dir / ".crewai_meta.json").write_text(json.dumps(meta, indent=2))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/src/crewai_cli/skills/main.py` at line 147, Remove the redundant
import statement "import json as _json": delete that import and update any
references to _json in this module to use the existing module-level "json"
import instead so the single top-level import is used consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/cli/src/crewai_cli/skills/main.py`:
- Around line 139-141: Remove the dead-import that tries to "ensure crewai_core
present" — delete the import block "from crewai_core.plus_api import PlusAPI as
_  # noqa: F401" in skills/main.py because plus_api_client is already used
earlier (self.plus_api_client.get_skill()) and the import is redundant and
misleading; simply remove that import line and its comment so the code does not
contain a no-op alias.
- Line 147: Remove the redundant import statement "import json as _json": delete
that import and update any references to _json in this module to use the
existing module-level "json" import instead so the single top-level import is
used consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9bc2b181-e205-4a5d-b205-5db5a1ad8826

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0372d and ab20a3a.

📒 Files selected for processing (7)
  • lib/cli/src/crewai_cli/cli.py
  • lib/cli/src/crewai_cli/git.py
  • lib/cli/src/crewai_cli/skills/main.py
  • lib/crewai-core/src/crewai_core/plus_api.py
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai/src/crewai/skills/cache.py
  • lib/crewai/src/crewai/skills/registry.py
✅ Files skipped from review due to trivial changes (1)
  • lib/cli/src/crewai_cli/git.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai-core/src/crewai_core/plus_api.py
  • lib/crewai/src/crewai/skills/cache.py
  • lib/cli/src/crewai_cli/cli.py
  • lib/crewai/src/crewai/skills/registry.py

- _parse_frontmatter() now delegates to crewai.skills.parser.parse_frontmatter
  when available, with a minimal fallback for CLI-only installs
- install() global cache path now reuses SkillCacheManager.store() instead
  of duplicating metadata writing logic
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/cli/src/crewai_cli/skills/main.py`:
- Around line 144-151: The fallback branch that unpacks to cache_dir (in main.py
around the except ImportError) must also write the same .crewai_meta.json
metadata that SkillCacheManager would produce so list_cached can discover the
skill; update the except block after self._unpack_archive(archive_bytes,
cache_dir) to create the metadata payload (including fields used by list_cached
such as ref, version, org, name, and any timestamps/ids the SDK path writes) and
atomically write it to cache_dir / ".crewai_meta.json" (mirroring the format
produced by SkillCacheManager); ensure the metadata keys and file name match
what list_cached expects so fallback-installed skills appear in crewi skill
list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f394393-5ccb-4f08-8bf0-653c3bc656b0

📥 Commits

Reviewing files that changed from the base of the PR and between ab20a3a and e3466f9.

📒 Files selected for processing (1)
  • lib/cli/src/crewai_cli/skills/main.py

Comment thread lib/cli/src/crewai_cli/skills/main.py
CodeRabbit caught that the ImportError fallback in install() didn't write
cache metadata, making skills invisible to 'crewai skill list'.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
lib/cli/src/crewai_cli/skills/main.py (2)

300-304: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

The pre-filter="data" TAR fallback is still link-traversal unsafe.

If extractall(..., filter="data") hits TypeError, _safe_extractall() only checks member.name and then hands the archive back to tf.extractall(dest). A tar with an in-tree symlink or hardlink can still pivot later members outside dest on runtimes that reach this fallback.

Suggested fix direction
 def _safe_extractall(tf: tarfile.TarFile, dest: Path) -> None:
     """Path-traversal-safe extraction for Python < 3.12."""
     dest_resolved = dest.resolve()
     for member in tf.getmembers():
         member_path = (dest / member.name).resolve()
         if not member_path.is_relative_to(dest_resolved):
             raise ValueError(f"Blocked path traversal attempt: {member.name!r}")
-    tf.extractall(dest)  # noqa: S202
+        if member.issym() or member.islnk() or not (member.isdir() or member.isfile()):
+            raise ValueError(f"Unsupported tar member: {member.name!r}")
+        if member.isdir():
+            member_path.mkdir(parents=True, exist_ok=True)
+            continue
+
+        member_path.parent.mkdir(parents=True, exist_ok=True)
+        src = tf.extractfile(member)
+        if src is None:
+            raise ValueError(f"Unreadable tar member: {member.name!r}")
+        member_path.write_bytes(src.read())

Please verify whether this fallback is reachable in supported runtimes. If the repo still supports Python 3.10 or 3.11, this path remains live.

#!/bin/bash
fd -HI '^(pyproject\.toml|setup\.cfg|setup\.py)$' . -t f | while read -r f; do
  echo "== $f =="
  rg -n 'requires-python|python_requires|Programming Language :: Python ::' "$f"
done

Also applies to: 367-374

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/src/crewai_cli/skills/main.py` around lines 300 - 304, The current
fallback on except TypeError should not call tf.extractall because that
reintroduces link-traversal risks; instead, replace the fallback so it uses a
secure extraction path: call your existing _safe_extractall(tf, dest) (and
ensure _safe_extractall does not delegate back to tf.extractall) and enhance
_safe_extractall to explicitly reject or sanitize members that are symlinks or
hard links (check TarInfo.issym()/islnk()), resolve member extraction targets
with os.path.realpath(os.path.join(dest, member.name)) and verify the realpath
starts with the intended dest realpath (reject if not), and handle link targets
that point outside by skipping/raising; make the same change in the second
occurrence (the other tarfile.extractall fallback block) so no fallback calls
tf.extractall in any runtime.

145-150: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback installs still won't show up in crewai skill list.

list_cached() only discovers global entries via .crewai_meta.json, but this ImportError branch unpacks the archive and never writes that file. In CLI-only environments, install succeeds and the next list call silently misses the skill.

Suggested fix
         except ImportError:
             # Fallback if SDK not installed — write directly
             cache_dir = Path.home() / ".crewai" / "skills" / org / name
             cache_dir.mkdir(parents=True, exist_ok=True)
             self._unpack_archive(archive_bytes, cache_dir)
+            (cache_dir / ".crewai_meta.json").write_text(
+                json.dumps({"org": org, "name": name, "version": version})
+            )
         console.print(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cli/src/crewai_cli/skills/main.py` around lines 145 - 150, The
ImportError fallback unpacks the skill to cache_dir via _unpack_archive but
doesn't create the .crewai_meta.json that list_cached() relies on, so add code
in the same except ImportError branch (inside the install flow that calls
_unpack_archive) to write the metadata file into cache_dir (e.g.,
".crewai_meta.json") with the same fields/structure used by list_cached(),
including org, name, version and any manifest/runtime fields list_cached
expects; ensure it uses safe JSON serialization and sets file permissions as
needed so CLI-only installs appear in list_cached().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/cli/src/crewai_cli/skills/main.py`:
- Around line 94-106: Ensure the skill ref strictly matches the single-segment
`@org/name` contract and cannot escape paths: before using
without_at.partition("/") require without_at.count("/") == 1, then validate that
both org and name are non-empty, do not contain any slashes, do not contain the
path-traversal segment "..", and do not start with a dot (e.g.
org.startswith(".") or name.startswith(".")). Reject the ref (console.print +
SystemExit(1)) if any check fails; keep using the existing without_at, org and
name variables and then safely build paths like Path("skills") / name and the
cache path using those validated values.

---

Duplicate comments:
In `@lib/cli/src/crewai_cli/skills/main.py`:
- Around line 300-304: The current fallback on except TypeError should not call
tf.extractall because that reintroduces link-traversal risks; instead, replace
the fallback so it uses a secure extraction path: call your existing
_safe_extractall(tf, dest) (and ensure _safe_extractall does not delegate back
to tf.extractall) and enhance _safe_extractall to explicitly reject or sanitize
members that are symlinks or hard links (check TarInfo.issym()/islnk()), resolve
member extraction targets with os.path.realpath(os.path.join(dest, member.name))
and verify the realpath starts with the intended dest realpath (reject if not),
and handle link targets that point outside by skipping/raising; make the same
change in the second occurrence (the other tarfile.extractall fallback block) so
no fallback calls tf.extractall in any runtime.
- Around line 145-150: The ImportError fallback unpacks the skill to cache_dir
via _unpack_archive but doesn't create the .crewai_meta.json that list_cached()
relies on, so add code in the same except ImportError branch (inside the install
flow that calls _unpack_archive) to write the metadata file into cache_dir
(e.g., ".crewai_meta.json") with the same fields/structure used by
list_cached(), including org, name, version and any manifest/runtime fields
list_cached expects; ensure it uses safe JSON serialization and sets file
permissions as needed so CLI-only installs appear in list_cached().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fbf6f74-4519-4abc-a094-90aae3871179

📥 Commits

Reviewing files that changed from the base of the PR and between e3466f9 and d966775.

📒 Files selected for processing (1)
  • lib/cli/src/crewai_cli/skills/main.py

Comment thread lib/cli/src/crewai_cli/skills/main.py
Reject refs with multiple slashes (@org/a/b), dot segments (@../skill),
or leading dots in org/name. Applied to both CLI install() and SDK
parse_registry_ref() so the contract is enforced consistently.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant