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
16 changed files with 58 additions and 564 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

@@ -376,27 +376,6 @@ def handle_internet_search(self) -> str:
...
```
### Naming handlers
The string in `@listen("…")` is a **router route label** (an event name), not the Python method name. Route labels and method completion events share one trigger namespace, so naming a handler the same as its route causes the handler to re-trigger itself in a loop.
Use a different method name — the docs examples use a `handle_*` prefix:
```python
@listen("create_video")
def handle_create_video(self) -> str:
"""User wants a new video."""
...
```
Do **not** mirror the route label on the method:
```python
@listen("create_video")
def create_video(self) -> str: # rejected at flow instantiation
...
```
…and the router LLM sees:
```

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 — do not name the handler after the route it serves.
### 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

@@ -39,8 +39,8 @@ Pick the simplest action that does the job.
- `state` is the initial shared data shape. Action results do not automatically merge into `state`.
- Read method results with `outputs.method_name` after that method can run.
- `listen` targets a method name or a router-emitted event name.
- Methods must not listen to their own method name — including when the `listen` value is a route label that matches the method name (e.g. `listen: create_video` on method `create_video`).
- Method names and emitted event names share one namespace. Do not reuse the same string for a method's `listen` target and its method name.
- Methods must not listen to their own method name.
- Method names and emitted event names share one namespace. Avoid reusing the same string for both unless the user explicitly wants that.
- Use `router: true` plus `emit` when one method chooses between named branches.
- A router action must return exactly one emitted event string. It must not return JSON, a list, or an explanation.
- Use `start: true` for the single entrypoint.
@@ -107,8 +107,8 @@ Dynamic value rules:
- Do not make `do` a list.
- Do not use CEL `+` to build text in action mappings. Keep the text literal and insert each dynamic value with `${...}`.
- Do not reference `outputs.some_method` before `some_method` can run.
- Do not set a method's `listen` to its own method name (including matching route labels such as `listen: create_video` on method `create_video`).
- Do not use the same string for a method's `listen` target and its method name.
- Do not set a method's `listen` to its own method name.
- Do not use the same string for an emitted event and a method name unless the user asks for it.
- Do not use `emit` without `router: true`.
- Do not rely on crew action-level `inputs` alone to ground agent behavior. Inputs that do not match placeholders are effectively unused by the prompt.
- Do not ask agents to infer missing facts when accuracy matters. Tell them to mark missing dates, amounts, offers, logs, or constraints as unknown.

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):

View File

@@ -4,7 +4,7 @@ import json
import logging
from typing import Any, ParamSpec, TypeVar
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel
from typing_extensions import TypeIs
from crewai.flow.flow_definition import (
@@ -432,20 +432,6 @@ def _iter_flow_methods(flow_class: type) -> dict[str, Any]:
return methods
def _flow_definition_validation_error(
flow_class: type, exc: ValidationError
) -> ValueError:
errors = exc.errors()
if errors:
detail = errors[0].get("msg", str(exc))
if isinstance(detail, str) and detail.startswith("Value error, "):
detail = detail.removeprefix("Value error, ")
else:
detail = str(exc)
class_name = getattr(flow_class, "__name__", "Flow")
return ValueError(f"Invalid flow definition for {class_name}: {detail}")
def _build_flow_definition_from_class(
flow_class: type,
namespace: dict[str, Any] | None = None,
@@ -469,18 +455,15 @@ def _build_flow_definition_from_class(
if docstring:
description = docstring.strip()
try:
definition = FlowDefinition(
name=getattr(flow_class, "__name__", "Flow"),
description=description,
state=_build_state_definition(flow_class),
config=_build_config_definition(flow_class),
persist=_build_persistence_definition(flow_class),
conversational=_build_conversational_definition(flow_class),
methods=methods,
)
except ValidationError as exc:
raise _flow_definition_validation_error(flow_class, exc) from exc
definition = FlowDefinition(
name=getattr(flow_class, "__name__", "Flow"),
description=description,
state=_build_state_definition(flow_class),
config=_build_config_definition(flow_class),
persist=_build_persistence_definition(flow_class),
conversational=_build_conversational_definition(flow_class),
methods=methods,
)
log_flow_definition_issues(definition)
return definition

View File

@@ -775,11 +775,7 @@ class FlowDefinition(BaseModel):
for method_name, method in self.methods.items():
if _condition_references(method.listen, method_name):
raise ValueError(
_self_listen_error(
method_name=method_name,
listen=method.listen,
definition=self,
)
f"methods.{method_name}.listen must not reference itself"
)
return self
@@ -892,39 +888,6 @@ def _condition_references(condition: FlowDefinitionCondition | None, name: str)
)
def _format_listen_condition(condition: FlowDefinitionCondition | None) -> str:
if condition is None:
return "None"
return repr(condition)
def _self_listen_error(
*,
method_name: str,
listen: FlowDefinitionCondition | None,
definition: FlowDefinition,
) -> str:
path = f"methods.{method_name}.listen"
listen_display = _format_listen_condition(listen)
conversational = (
definition.conversational is not None and definition.conversational.enabled
)
if conversational:
return (
f"{path} listen condition {listen_display} matches the handler name "
f"{method_name!r}. In conversational flows, @listen labels are router "
"route names — they share the same trigger namespace as method completion "
"events, so this handler would re-run in a loop. Rename the handler "
f"(for example, handle_{method_name}) or use a different route label."
)
return (
f"{path} listen condition {listen_display} references the handler name "
f"{method_name!r}. A listener triggered by its own completion creates an "
"infinite loop. Listen to a different method or event, or rename the handler."
)
def _validate_action_cel(
action: FlowActionDefinition,
*,

View File

@@ -2158,7 +2158,7 @@ def test_self_listening_method_is_rejected():
def process(self):
pass
with pytest.raises(ValueError, match="Invalid flow definition for SelfListenFlow"):
with pytest.raises(ValueError, match="methods.process.listen"):
SelfListenFlow.flow_definition()
@@ -2176,7 +2176,7 @@ def test_or_condition_self_listen_is_rejected():
def process(self):
pass
with pytest.raises(ValueError, match="Invalid flow definition for OrSelfListenFlow"):
with pytest.raises(ValueError, match="methods.process.listen"):
OrSelfListenFlow.flow_definition()
@@ -2190,7 +2190,7 @@ def test_router_self_listening_method_is_rejected():
def route(self):
return "done"
with pytest.raises(ValueError, match="Invalid flow definition for RouterSelfListenFlow"):
with pytest.raises(ValueError, match="methods.route.listen"):
RouterSelfListenFlow.flow_definition()

View File

@@ -1231,7 +1231,7 @@ def test_static_string_listener_is_allowed_by_contract():
@pytest.mark.parametrize("listen", ["publish", {"or": ["publish", "revise"]}])
@pytest.mark.parametrize("router_enabled", [False, True])
def test_flow_definition_rejects_method_self_listen(listen, router_enabled):
with pytest.raises(ValueError, match="listen condition"):
with pytest.raises(ValueError, match="methods.publish.listen"):
flow_definition.FlowDefinition.from_declaration(contents=
{
"schema": "crewai.flow/v1",
@@ -1252,49 +1252,6 @@ def test_flow_definition_rejects_method_self_listen(listen, router_enabled):
)
def test_flow_definition_rejects_conversational_route_handler_name_collision():
with pytest.raises(ValueError, match=r"listen condition 'create_video'"):
flow_definition.FlowDefinition.from_declaration(contents=
{
"schema": "crewai.flow/v1",
"name": "VideoFlow",
"conversational": {
"enabled": True,
"router": {
"route_descriptions": {
"create_video": "User wants a new video.",
},
},
},
"methods": {
"begin": {
"do": {"ref": "loaded_flows:VideoFlow.begin"},
"start": True,
},
"create_video": {
"do": {"ref": "loaded_flows:VideoFlow.create_video"},
"listen": "create_video",
},
},
}
)
def test_build_flow_definition_wraps_validation_error_with_class_name():
class VideoFlow(Flow):
conversational = True
@listen("create_video")
def create_video(self):
return "made a video"
with pytest.raises(ValueError, match="Invalid flow definition for VideoFlow"):
VideoFlow.flow_definition()
with pytest.raises(ValueError, match="Invalid flow definition for VideoFlow"):
VideoFlow()
def test_start_false_not_classified_as_start_method():
definition = flow_definition.FlowDefinition.from_declaration(contents=
{