Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
f99bafa429 fix: clear CodeQL weak sensitive hashing on model catalog cache key
SHA-256 of API key material in _cache_key trips
py/weak-sensitive-data-hashing (CodeQL alert #64). Use HMAC-SHA256 with
the credential as the key and a fixed app message so the cache partition
id stays non-reversible without password-style hashing.

Co-authored-by: Rip&Tear <theCyberTech@users.noreply.github.com>
2026-08-05 13:21:29 +00:00
10 changed files with 39 additions and 427 deletions

View File

@@ -36,73 +36,24 @@ crewai [COMMAND] [OPTIONS] [ARGUMENTS]
### 1. Create
Create a new crew, flow, tool, skill, or template project.
Create a new crew or flow.
```shell Terminal
crewai create [OPTIONS] TYPE NAME
```
- `TYPE`: `crew`, `flow`, `tool`, `skill`, or `template`
- `NAME`: Name of the project, tool handle, skill, or template
- `TYPE`: Choose between "crew" or "flow"
- `NAME`: Name of the crew or flow
#### Crew
Example:
```shell Terminal
crewai create crew my_new_crew
crewai create crew my_new_crew --classic
crewai create flow my_new_flow
```
By default, `crewai create crew` creates a JSON-first crew project with `crew.jsonc` and `agents/*.jsonc`. Use `crewai create crew my_new_crew --classic` only when you want the older Python/YAML scaffold with `crew.py`, `config/agents.yaml`, and `config/tasks.yaml`.
#### Flow
```shell Terminal
crewai create flow my_new_flow
crewai create flow my_new_flow --declarative
```
#### Tool
Scaffold a custom tool repository:
```shell Terminal
crewai create tool my_tool
```
#### Skill
Scaffold an agent skill. Inside a crew project (where `pyproject.toml` exists), the skill is created under `./skills/`:
```shell Terminal
crewai create skill my-skill
crewai create skill my-skill --no-project
```
Use `--no-project` to create the skill in the current directory instead of `./skills/`.
#### Template
Add a remote project template to the current directory:
```shell Terminal
crewai create template my-template
crewai create template my-template --output-dir custom_dir
```
Use `--output-dir` to override the output folder name (defaults to the template name).
#### Deprecated create aliases
These older commands still work but print a yellow deprecation warning. Prefer the `crewai create <type>` forms above.
| Deprecated | Use instead |
| :--- | :--- |
| `crewai tool create <handle>` | `crewai create tool <handle>` |
| `crewai skill create <name>` | `crewai create skill <name>` |
| `crewai template add <name>` | `crewai create template <name>` |
Lifecycle commands are unchanged — for example `crewai tool install`, `crewai skill publish`, and `crewai template list` stay under their resource groups.
### 2. Version
Show the installed version of CrewAI.

View File

@@ -32,10 +32,10 @@ You often need **both**: skills for expertise, tools for action. They are config
The CLI is the supported way to create a skill — it scaffolds the directory layout and a valid `SKILL.md` for you:
```shell Terminal
crewai create skill code-review
crewai skill create code-review
```
Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project` on `crewai create skill`):
Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project`):
```
skills/
@@ -178,16 +178,12 @@ agent = Agent(
## Creating, Publishing, and Installing Skills
Skills have a full lifecycle managed by the CLI: **create them with `crewai create skill`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid.
<Note>
`crewai skill create` is deprecated and still works with a warning. Use `crewai create skill` instead.
</Note>
Skills have a full lifecycle managed by the CLI: **create them with `crewai skill create`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid.
### Create
```shell Terminal
crewai create skill my-skill
crewai skill create my-skill
```
Scaffolds the directory (into `./skills/` inside a crew project) with a template `SKILL.md`, plus empty `scripts/`, `references/`, and `assets/` directories. Edit `SKILL.md` to define the instructions.

View File

@@ -21,13 +21,9 @@ crewai create crew my_crew
crewai create flow my_flow
# Tool repository
crewai create tool my_tool
crewai tool create my_tool
```
<Note>
`crewai tool create` is deprecated and still works with a warning. Use `crewai create tool` instead.
</Note>
## Tool Setup: Point Assistants to AGENTS.md
### Codex

View File

@@ -19,7 +19,6 @@ from crewai_cli.utils import (
enable_prompt_line_editing,
is_dmn_mode_enabled,
read_toml,
warn_deprecated_command,
)
@@ -138,10 +137,7 @@ def uv(uv_args: tuple[str, ...]) -> None:
@crewai.command()
@click.argument(
"type",
required=False,
default=None,
type=click.Choice(["crew", "flow", "tool", "skill", "template"]),
"type", required=False, default=None, type=click.Choice(["crew", "flow"])
)
@click.argument("name", required=False, default=None)
@click.option("--provider", type=str, help="The provider to use for the crew")
@@ -156,21 +152,6 @@ def uv(uv_args: tuple[str, ...]) -> None:
is_flag=True,
help="Create a declarative Flow project instead of a Python Flow project",
)
@click.option(
"--no-project",
"in_project",
is_flag=True,
default=True,
flag_value=False,
help="Skill only: create in current dir instead of ./skills/",
)
@click.option(
"-o",
"--output-dir",
type=str,
default=None,
help="Template only: directory name for the template (defaults to template name)",
)
def create(
type: str | None,
name: str | None,
@@ -178,17 +159,14 @@ def create(
skip_provider: bool = False,
classic: bool = False,
declarative: bool = False,
in_project: bool = True,
output_dir: str | None = None,
) -> None:
"""Create a new crew, flow, tool, skill, or template."""
"""Create a new crew, or flow."""
dmn_mode = is_dmn_mode_enabled()
if not type:
if dmn_mode:
raise click.UsageError(
"TYPE is required when CREWAI_DMN is set. "
"Use `crewai create <type> <name>` where type is one of: "
"crew, flow, tool, skill, template."
"Use `crewai create crew <name>` or `crewai create flow <name>`."
)
from crewai_cli.tui_picker import pick
@@ -198,9 +176,6 @@ def create(
"flow",
"A deterministic workflow with full control over agents and crews",
),
("tool", "A custom tool for the CrewAI Tool Repository"),
("skill", "An agent skill with instructions and optional assets"),
("template", "A remote project template from the CrewAI gallery"),
]
type = pick("What would you like to create?", options)
if type is None:
@@ -214,36 +189,9 @@ def create(
click.style(f" Name of your {type}", fg="cyan", bold=True),
prompt_suffix=click.style(" ", fg="bright_white"), # noqa: RUF001
)
if dmn_mode and type == "crew":
if dmn_mode:
skip_provider = True
if not in_project and type != "skill":
raise click.UsageError("--no-project can only be used with skill projects.")
if output_dir is not None and type != "template":
raise click.UsageError("--output-dir can only be used with template projects.")
if type == "tool":
if declarative or classic or provider is not None or skip_provider:
raise click.UsageError(
"Crew and flow options cannot be used with tool projects."
)
from crewai_cli.tools.main import ToolCommand
ToolCommand().create(name)
elif type == "skill":
if declarative or classic or provider is not None or skip_provider:
raise click.UsageError(
"Crew and flow options cannot be used with skill projects."
)
from crewai_cli.skills.main import SkillCommand
SkillCommand().create(name, in_project=in_project)
elif type == "template":
if declarative or classic or provider is not None or skip_provider:
raise click.UsageError(
"Crew and flow options cannot be used with template projects."
)
template_cmd = TemplateCommand()
template_cmd.add_template(name, output_dir)
elif type == "crew":
if type == "crew":
if declarative:
raise click.UsageError("--declarative can only be used with flow projects")
if classic:
@@ -259,10 +207,7 @@ def create(
create_flow(name, declarative=declarative)
else:
click.secho(
"Error: Invalid type. Must be 'crew', 'flow', 'tool', 'skill', or 'template'.",
fg="red",
)
click.secho("Error: Invalid type. Must be 'crew' or 'flow'.", fg="red")
@crewai.command()
@@ -707,8 +652,6 @@ def tool() -> None:
@tool.command(name="create")
@click.argument("handle")
def tool_create(handle: str) -> None:
"""[Deprecated: use `crewai create tool`] Create a custom tool project."""
warn_deprecated_command(old="crewai tool create", new="crewai create tool")
from crewai_cli.tools.main import ToolCommand
tool_cmd = ToolCommand()
@@ -759,8 +702,6 @@ def skill() -> None:
help="Create skill in current dir instead of ./skills/",
)
def skill_create(name: str, in_project: bool) -> None:
"""[Deprecated: use `crewai create skill`] Create a new agent skill."""
warn_deprecated_command(old="crewai skill create", new="crewai create skill")
from crewai_cli.skills.main import SkillCommand
skill_cmd = SkillCommand()
@@ -824,8 +765,7 @@ def template_list() -> None:
help="Directory name for the template (defaults to template name)",
)
def template_add(name: str, output_dir: str | None) -> None:
"""[Deprecated: use `crewai create template`] Add a template to the current directory."""
warn_deprecated_command(old="crewai template add", new="crewai create template")
"""Add a template to the current directory."""
template_cmd = TemplateCommand()
template_cmd.add_template(name, output_dir)

View File

@@ -25,6 +25,7 @@ from __future__ import annotations
from collections.abc import Callable
import contextlib
import hashlib
import hmac
import json
import os
from pathlib import Path
@@ -626,7 +627,14 @@ def _cache_key(provider_key: str) -> str:
api_key = _provider_api_key(provider_key)
if not api_key:
return f"{provider_key}#nokey"
digest = hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:12]
# HMAC with the credential as the key (not as hash input). SHA-256 alone on
# API-key material trips CodeQL py/weak-sensitive-data-hashing; keyed HMAC is
# the right construction for a local cache partition id.
digest = hmac.new(
api_key.encode("utf-8"),
b"crewai.model_catalog.cache_v1",
hashlib.sha256,
).hexdigest()[:12]
return f"{provider_key}#{digest}"

View File

@@ -40,14 +40,6 @@ This ensures generated code always matches the version actually installed, not s
-`Agent(llm=ChatOpenAI(...))` → ✅ `Agent(llm="openai/gpt-4o")` or `Agent(llm=LLM(model="..."))`
- ❌ Passing raw OpenAI client objects → ✅ Use `crewai.LLM` wrapper
### Deprecated CLI scaffolding aliases (still supported)
These commands remain supported but print a yellow deprecation warning. Prefer the canonical forms:
- ⚠️ `crewai tool create <handle>` → ✅ `crewai create tool <handle>`
- ⚠️ `crewai skill create <name>` → ✅ `crewai create skill <name>`
- ⚠️ `crewai template add <name>` → ✅ `crewai create template <name>`
### How to verify you're using current patterns:
1. You ran the version check and docs lookup steps above before writing code
2. All LLM references use `crewai.LLM` or string shorthand (`"openai/gpt-4o"`)
@@ -145,26 +137,8 @@ uv sync # Sync dependencies
uv lock # Lock dependencies
# Project scaffolding
crewai create crew <name> --skip_provider # New crew project
crewai create flow <name> # New flow project
crewai create tool <handle> # Custom tool repository
crewai create skill <name> # Agent skill (./skills/ in crew projects)
crewai create skill <name> --no-project # Skill in current directory
crewai create template <name> # Remote project template
crewai create template <name> -o <output_dir> # Template with custom output directory
# Deprecated scaffolding aliases (still work; print a yellow warning)
# crewai tool create <handle> → crewai create tool <handle>
# crewai skill create <name> → crewai create skill <name>
# crewai template add <name> → crewai create template <name>
# Tool, skill, and template lifecycle (unchanged)
crewai tool install <handle>
crewai tool publish
crewai skill install @org/name
crewai skill publish
crewai skill list
crewai template list
crewai create crew <name> --skip_provider # New crew project
crewai create flow <name> --skip_provider # New flow project
# Running
crewai run # Run crew or flow (auto-detects from pyproject.toml)
@@ -653,26 +627,6 @@ class MyFlow(Flow):
| `@listen(method)` | Triggers when specified method completes. Receives output as argument |
| `@router(method)` | Conditional branching. Returns string labels that trigger `@listen("label")` |
### `@listen` labels vs handler names
The string in `@listen("...")` is an **event or route label**, not the Python method name. Router return values, route labels, and method completion events share one trigger namespace.
**Never** use the same name for the `@listen` label and the handler method:
```python
# ❌ Wrong — raises a validation error when the flow is instantiated
@listen("create_video")
def create_video(self):
...
# ✅ Correct — distinct handler name (handle_* prefix is a common pattern)
@listen("create_video")
def handle_create_video(self):
...
```
If validation were bypassed, matching names would also cause the handler to re-trigger itself in a loop at runtime. This applies to all flows; it is especially common in **conversational flows** (`conversational = True`), where `@listen("...")` is a router intent name.
### Structured State
```python
from pydantic import BaseModel
@@ -1159,9 +1113,7 @@ Python >=3.10, <3.14
```bash
uv tool install crewai # Install CrewAI CLI
uv tool list # Verify installation
crewai create crew my_crew --skip_provider # Scaffold a crew project
crewai create tool my_tool # Scaffold a tool repository
crewai create skill my_skill # Scaffold an agent skill
crewai create crew my_crew --skip_provider # Scaffold a new project
crewai install # Install project dependencies
crewai run # Execute
```
@@ -1196,4 +1148,3 @@ crewai run # Execute
- Using `process=Process.hierarchical` without setting `manager_llm` or `manager_agent`
- Circular delegation: set `allow_delegation=False` on specialist agents
- Not installing tools package: `uv add crewai-tools`
- **Matching `@listen("label")` to the handler method name** — raises a validation error at flow instantiation; would re-trigger in an infinite loop at runtime only if validation is bypassed. Use a different method name (e.g. `handle_create_video` for `@listen("create_video")`)

View File

@@ -44,19 +44,10 @@ __all__ = [
"render_template",
"tree_copy",
"tree_find_and_replace",
"warn_deprecated_command",
"write_env_file",
]
def warn_deprecated_command(*, old: str, new: str) -> None:
"""Print a yellow deprecation warning for a legacy CLI command path."""
click.secho(
f"Warning: The command '{old}' is deprecated. Use '{new}' instead.",
fg="yellow",
)
console = Console()
_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}")

View File

@@ -228,7 +228,6 @@ def test_create_requires_type_in_dmn_mode(runner):
assert result.exit_code == 2
assert "TYPE is required when CREWAI_DMN is set" in result.output
assert "crew, flow, tool, skill, template" in result.output
def test_create_requires_name_in_dmn_mode(runner):

View File

@@ -1,230 +0,0 @@
"""Tests for unified `crewai create <resource>` scaffolding commands."""
from unittest import mock
import pytest
from click.testing import CliRunner
from crewai_cli.cli import create, crewai
@pytest.fixture
def runner():
return CliRunner()
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_create_tool_invokes_tool_command(mock_tool_command_cls, runner):
result = runner.invoke(create, ["tool", "my_tool"])
assert result.exit_code == 0, result.output
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
assert "deprecated" not in result.output.lower()
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_tool_create_is_deprecated_and_still_works(mock_tool_command_cls, runner):
result = runner.invoke(crewai, ["tool", "create", "my_tool"])
assert result.exit_code == 0, result.output
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
assert (
"Warning: The command 'crewai tool create' is deprecated. "
"Use 'crewai create tool' instead."
in result.output
)
@mock.patch("crewai_cli.tools.main.ToolCommand")
@pytest.mark.parametrize("extra_args", [["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]])
def test_create_tool_rejects_crew_and_flow_flags(mock_tool_command_cls, runner, extra_args):
result = runner.invoke(create, ["tool", "my_tool", *extra_args])
assert result.exit_code == 2, result.output
assert "Crew and flow options cannot be used with tool projects." in result.output
mock_tool_command_cls.return_value.create.assert_not_called()
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_skill_invokes_skill_command(mock_skill_command_cls, runner):
result = runner.invoke(create, ["skill", "my-skill"])
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=True
)
assert "deprecated" not in result.output.lower()
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_skill_no_project_flag(mock_skill_command_cls, runner):
result = runner.invoke(create, ["skill", "my-skill", "--no-project"])
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=False
)
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_skill_create_is_deprecated_and_still_works(mock_skill_command_cls, runner):
result = runner.invoke(crewai, ["skill", "create", "my-skill"])
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=True
)
assert (
"Warning: The command 'crewai skill create' is deprecated. "
"Use 'crewai create skill' instead."
in result.output
)
@mock.patch("crewai_cli.skills.main.SkillCommand")
@pytest.mark.parametrize(
"extra_args",
[["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]],
)
def test_create_skill_rejects_crew_and_flow_flags(
mock_skill_command_cls, runner, extra_args
):
result = runner.invoke(create, ["skill", "my-skill", *extra_args])
assert result.exit_code == 2, result.output
assert "Crew and flow options cannot be used with skill projects." in result.output
mock_skill_command_cls.return_value.create.assert_not_called()
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_crew_rejects_no_project_flag(mock_skill_command_cls, runner):
result = runner.invoke(create, ["crew", "my-crew", "--no-project"])
assert result.exit_code == 2, result.output
assert "--no-project can only be used with skill projects." in result.output
mock_skill_command_cls.return_value.create.assert_not_called()
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_create_template_invokes_template_command(mock_template_command_cls, runner):
result = runner.invoke(create, ["template", "my-template"])
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", None
)
assert "deprecated" not in result.output.lower()
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_create_template_output_dir_flag(mock_template_command_cls, runner):
result = runner.invoke(
create, ["template", "my-template", "--output-dir", "custom_dir"]
)
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", "custom_dir"
)
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_template_add_is_deprecated_and_still_works(mock_template_command_cls, runner):
result = runner.invoke(
crewai, ["template", "add", "my-template", "--output-dir", "custom_dir"]
)
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", "custom_dir"
)
assert (
"Warning: The command 'crewai template add' is deprecated. "
"Use 'crewai create template' instead."
in result.output
)
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
@pytest.mark.parametrize(
"extra_args",
[["--classic"], ["--declarative"], ["--provider", "openai"], ["--skip_provider"]],
)
def test_create_template_rejects_crew_and_flow_flags(
mock_template_command_cls, runner, extra_args
):
result = runner.invoke(create, ["template", "my-template", *extra_args])
assert result.exit_code == 2, result.output
assert (
"Crew and flow options cannot be used with template projects."
in result.output
)
mock_template_command_cls.return_value.add_template.assert_not_called()
@mock.patch("crewai_cli.remote_template.main.TemplateCommand")
def test_create_crew_rejects_output_dir_flag(mock_template_command_cls, runner):
result = runner.invoke(create, ["crew", "my-crew", "--output-dir", "custom_dir"])
assert result.exit_code == 2, result.output
assert "--output-dir can only be used with template projects." in result.output
mock_template_command_cls.return_value.add_template.assert_not_called()
@mock.patch("crewai_cli.cli.enable_prompt_line_editing")
@mock.patch("crewai_cli.cli.click.prompt", return_value="picked-tool")
@mock.patch("crewai_cli.tui_picker.pick", return_value="tool")
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_create_picker_supports_tool_skill_and_template(
mock_tool_command_cls,
mock_pick,
mock_prompt,
mock_enable_prompt,
runner,
):
result = runner.invoke(create, [])
assert result.exit_code == 0, result.output
mock_pick.assert_called_once()
picker_options = mock_pick.call_args[0][1]
assert {option[0] for option in picker_options} == {
"crew",
"flow",
"tool",
"skill",
"template",
}
mock_prompt.assert_called_once()
mock_tool_command_cls.return_value.create.assert_called_once_with("picked-tool")
_DMN_ENV = {"CREWAI_DMN": "True"}
@mock.patch("crewai_cli.tools.main.ToolCommand")
def test_create_tool_works_in_dmn_mode(mock_tool_command_cls, runner):
result = runner.invoke(create, ["tool", "my_tool"], env=_DMN_ENV)
assert result.exit_code == 0, result.output
mock_tool_command_cls.return_value.create.assert_called_once_with("my_tool")
@mock.patch("crewai_cli.skills.main.SkillCommand")
def test_create_skill_works_in_dmn_mode(mock_skill_command_cls, runner):
result = runner.invoke(create, ["skill", "my-skill"], env=_DMN_ENV)
assert result.exit_code == 0, result.output
mock_skill_command_cls.return_value.create.assert_called_once_with(
"my-skill", in_project=True
)
@mock.patch("crewai_cli.cli.TemplateCommand")
def test_create_template_works_in_dmn_mode(mock_template_command_cls, runner):
result = runner.invoke(create, ["template", "my-template"], env=_DMN_ENV)
assert result.exit_code == 0, result.output
mock_template_command_cls.return_value.add_template.assert_called_once_with(
"my-template", None
)

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
import hashlib
import hmac
import json
import time
@@ -583,6 +585,14 @@ def test_cache_key_hashes_key_and_never_stores_it(monkeypatch):
key = mc._cache_key("openai")
assert key.startswith("openai#") and key != "openai#nokey"
assert "sk-super-secret" not in key # only a digest, never the raw key
# Credential is the HMAC key (not SHA-256 hash input) so CodeQL
# py/weak-sensitive-data-hashing does not flag password-style hashing.
expected = hmac.new(
b"sk-super-secret",
b"crewai.model_catalog.cache_v1",
hashlib.sha256,
).hexdigest()[:12]
assert key == f"openai#{expected}"
def test_dynamic_cache_expires_after_catalog_ttl(monkeypatch):