mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-26 18:48:10 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e10c17fcf6 | ||
|
|
f364a7d988 | ||
|
|
2771c02f45 | ||
|
|
5d4851eac7 |
@@ -5,15 +5,49 @@ icon: wrench
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 18,
|
||||
padding: "24px",
|
||||
marginBottom: 32,
|
||||
borderRadius: 12,
|
||||
border: "1px solid rgba(235,102,88,0.28)",
|
||||
background: "linear-gradient(180deg, rgba(235,102,88,0.14) 0%, rgba(235,102,88,0.06) 100%)"
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p style={{ margin: 0, color: "#EB6658", fontSize: 13, fontWeight: 700, textTransform: "uppercase" }}>
|
||||
Coding agent setup
|
||||
</p>
|
||||
<h2 style={{ margin: "6px 0 8px" }}>Set up CrewAI in your coding agent</h2>
|
||||
<p style={{ margin: 0, color: "var(--mint-text-2)", maxWidth: 760 }}>
|
||||
Copy a ready-to-paste setup prompt for Claude Code, Codex, Cursor, or any coding agent. It installs the official CrewAI skills, checks the CLI, and points the agent at the right docs before it edits code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI.
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="button button-primary"
|
||||
onClick={async (event) => {
|
||||
const prompt = `Set up this environment so I can build with CrewAI.
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 12, alignItems: "center" }}>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid #EB6658",
|
||||
background: "#EB6658",
|
||||
color: "#FFFFFF",
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
cursor: "pointer",
|
||||
boxShadow: "0 10px 24px rgba(235,102,88,0.22)"
|
||||
}}
|
||||
onClick={async (event) => {
|
||||
const prompt = `Set up this environment so I can build with CrewAI.
|
||||
|
||||
First install the official CrewAI coding-agent skills if this environment supports npx:
|
||||
|
||||
@@ -48,21 +82,49 @@ Setup steps:
|
||||
Do not hardcode API keys. Use .env.
|
||||
Do not invent CLI flags. Validate with crewai --help or crewai create --help.
|
||||
If a command fails, show the exact command and error, explain the likely cause, fix what you can safely fix, and retry once.`;
|
||||
const button = event.currentTarget;
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
button.textContent = "Copied";
|
||||
} catch {
|
||||
button.textContent = "Copy failed";
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
button.textContent = "Copy instructions for coding agents";
|
||||
}, 1600);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy instructions for coding agents
|
||||
</button>
|
||||
const button = event.currentTarget;
|
||||
const resetTimeout = button.dataset.resetTimeout;
|
||||
if (resetTimeout) {
|
||||
window.clearTimeout(Number(resetTimeout));
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
button.textContent = "Copied";
|
||||
} catch {
|
||||
button.textContent = "Copy failed";
|
||||
} finally {
|
||||
button.dataset.resetTimeout = String(window.setTimeout(() => {
|
||||
button.textContent = "Copy agent setup prompt";
|
||||
delete button.dataset.resetTimeout;
|
||||
}, 1600));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy agent setup prompt
|
||||
</button>
|
||||
<a
|
||||
href="/en/guides/coding-tools/build-with-ai"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.36)",
|
||||
color: "#EB6658",
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
View coding-agent guide
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Watch: Building CrewAI Agents & Flows with Coding Agent Skills
|
||||
|
||||
<iframe src="https://www.loom.com/embed/befb9f68b81f42ad8112bfdd95a780af" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style={{width: "100%", height: "400px"}}></iframe>
|
||||
|
||||
|
||||
130
docs/index.mdx
130
docs/index.mdx
@@ -27,9 +27,133 @@ mode: "wide"
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center' }}>
|
||||
<a className="button button-primary" href="/en/quickstart">Get started</a>
|
||||
<a className="button" href="/en/changelog">View changelog</a>
|
||||
<a className="button" href="/en/api-reference/introduction">API Reference</a>
|
||||
<a
|
||||
href="/en/quickstart"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid #EB6658",
|
||||
background: "#EB6658",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.36)",
|
||||
background: "rgba(235,102,88,0.08)",
|
||||
color: "#EB6658",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={async (event) => {
|
||||
const prompt = `Set up this environment so I can build with CrewAI.
|
||||
|
||||
First install the official CrewAI coding-agent skills if this environment supports npx:
|
||||
|
||||
npx skills add crewaiinc/skills
|
||||
|
||||
If npx is missing or the current agent cannot load skills, do not fail the whole setup. Report the exact issue and continue using the CrewAI docs directly.
|
||||
|
||||
Use these CrewAI docs as source of truth before making assumptions:
|
||||
- https://skills.crewai.com
|
||||
- https://docs.crewai.com/llms.txt
|
||||
- https://docs.crewai.com/en/installation
|
||||
- https://docs.crewai.com/en/guides/coding-tools/build-with-ai
|
||||
|
||||
Setup steps:
|
||||
1. Check python3 --version. CrewAI requires Python >=3.10 and <3.14.
|
||||
2. Install uv if missing:
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
3. Source the uv environment if needed:
|
||||
source "$HOME/.local/bin/env"
|
||||
4. Install the CrewAI CLI:
|
||||
uv tool install crewai
|
||||
5. Verify the CLI:
|
||||
crewai version
|
||||
crewai create --help
|
||||
6. Create a project:
|
||||
CREWAI_DMN=true crewai create
|
||||
7. After project creation, inspect the generated files before editing.
|
||||
8. Run:
|
||||
crewai install
|
||||
crewai run
|
||||
|
||||
Do not hardcode API keys. Use .env.
|
||||
Do not invent CLI flags. Validate with crewai --help or crewai create --help.
|
||||
If a command fails, show the exact command and error, explain the likely cause, fix what you can safely fix, and retry once.`;
|
||||
const button = event.currentTarget;
|
||||
const resetTimeout = button.dataset.resetTimeout;
|
||||
if (resetTimeout) {
|
||||
window.clearTimeout(Number(resetTimeout));
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
button.textContent = "Copied";
|
||||
} catch {
|
||||
button.textContent = "Copy failed";
|
||||
} finally {
|
||||
button.dataset.resetTimeout = String(window.setTimeout(() => {
|
||||
button.textContent = "Copy agent setup prompt";
|
||||
delete button.dataset.resetTimeout;
|
||||
}, 1600));
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy agent setup prompt
|
||||
</button>
|
||||
<a
|
||||
href="/guides/coding-tools/build-with-ai"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.28)",
|
||||
color: "#EB6658",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
Coding-agent guide
|
||||
</a>
|
||||
<a
|
||||
href="/en/api-reference/introduction"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 42,
|
||||
padding: "0 16px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(235,102,88,0.18)",
|
||||
color: "var(--mint-text-2)",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
API Reference
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ import shutil
|
||||
import click
|
||||
from crewai_core.telemetry import Telemetry
|
||||
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
DECLARATIVE_FLOW_FOLDERS = ("crews", "tools", "knowledge", "skills")
|
||||
|
||||
@@ -71,6 +73,9 @@ def _create_python_flow(
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{flow_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
content = content.replace(
|
||||
"{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
|
||||
with open(dst_file, "w") as file:
|
||||
file.write(content)
|
||||
@@ -138,6 +143,9 @@ def _create_declarative_flow(
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{flow_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
content = content.replace(
|
||||
"{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
dst_file.write_text(content, encoding="utf-8")
|
||||
|
||||
(project_root / ".env").write_text("OPENAI_API_KEY=YOUR_API_KEY", encoding="utf-8")
|
||||
|
||||
@@ -20,6 +20,7 @@ from crewai_cli.utils import (
|
||||
load_env_vars,
|
||||
write_env_file,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
# ── Provider / model data ───────────────────────────────────────
|
||||
@@ -89,7 +90,7 @@ description = "{name} using crewAI"
|
||||
authors = [{{ name = "Your Name", email = "you@example.com" }}]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.8a1"
|
||||
"{crewai_tools_dependency}"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
@@ -1134,7 +1135,11 @@ def create_json_crew(
|
||||
|
||||
# Write pyproject.toml
|
||||
(folder_path / "pyproject.toml").write_text(
|
||||
_PYPROJECT_TOML.format(folder_name=folder_name, name=name),
|
||||
_PYPROJECT_TOML.format(
|
||||
folder_name=folder_name,
|
||||
name=name,
|
||||
crewai_tools_dependency=get_crewai_tools_dependency(),
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
import webbrowser
|
||||
|
||||
from crewai_core.plus_api import CreateCrewPayload
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.command import BaseCommand, PlusAPIMixin
|
||||
from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai_cli.deploy.archive import create_project_zip
|
||||
from crewai_cli.deploy.validate import DeployValidator, Severity, render_report
|
||||
from crewai_cli.utils import fetch_and_json_env_file, get_project_name
|
||||
@@ -14,6 +17,8 @@ from crewai_cli.utils import fetch_and_json_env_file, get_project_name
|
||||
|
||||
console = Console()
|
||||
_MISSING_LOCKFILE_ERROR_CODES = {"missing_lockfile"}
|
||||
_DEPLOYMENT_ID_KEYS = ("deployment_id", "deploymentId")
|
||||
_DEPLOYMENT_FALLBACK_IDENTIFIER_KEYS = ("id", "uuid")
|
||||
|
||||
|
||||
def _run_predeploy_validation(
|
||||
@@ -79,6 +84,39 @@ def _env_summary(env_vars: dict[str, str]) -> str:
|
||||
return f"{len(env_vars)} env vars: {keys}"
|
||||
|
||||
|
||||
def _deployment_identifier(json_response: dict[str, Any]) -> str | None:
|
||||
"""Return the best available identifier for a deployment show URL."""
|
||||
deployment = json_response.get("deployment")
|
||||
|
||||
for key in _DEPLOYMENT_ID_KEYS:
|
||||
value = json_response.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
if isinstance(deployment, dict):
|
||||
for key in _DEPLOYMENT_ID_KEYS + _DEPLOYMENT_FALLBACK_IDENTIFIER_KEYS:
|
||||
value = deployment.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
for key in _DEPLOYMENT_FALLBACK_IDENTIFIER_KEYS:
|
||||
value = json_response.get(key)
|
||||
if value:
|
||||
return str(value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _deployment_page_url(base_url: str, json_response: dict[str, Any]) -> str | None:
|
||||
"""Build the CrewAI deployment show URL for a response payload."""
|
||||
identifier = _deployment_identifier(json_response)
|
||||
if not identifier:
|
||||
return None
|
||||
return (
|
||||
f"{base_url.rstrip('/')}/crewai_plus/deployments/{quote(identifier, safe='')}"
|
||||
)
|
||||
|
||||
|
||||
def _needs_lockfile_for_deploy(project_root: Path | None = None) -> bool:
|
||||
"""Return True when deploy should create the project's first lockfile."""
|
||||
root = project_root or Path.cwd()
|
||||
@@ -165,6 +203,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
console.print("crewai deploy status")
|
||||
console.print(" or")
|
||||
console.print(f'crewai deploy status --uuid "{json_response["uuid"]}"')
|
||||
self._open_deployment_page(json_response)
|
||||
|
||||
def _display_logs(self, log_messages: list[dict[str, Any]]) -> None:
|
||||
"""
|
||||
@@ -178,6 +217,28 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
f"{log_message['timestamp']} - {log_message['level']}: {log_message['message']}"
|
||||
)
|
||||
|
||||
def _open_deployment_page(self, json_response: dict[str, Any]) -> None:
|
||||
"""Open the deployment show page in the user's browser when possible."""
|
||||
base_url = str(
|
||||
getattr(self.plus_api_client, "base_url", None)
|
||||
or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
)
|
||||
deployment_url = _deployment_page_url(base_url, json_response)
|
||||
if not deployment_url:
|
||||
return
|
||||
|
||||
console.print(f"\nOpening deployment page: [blue]{deployment_url}[/blue]")
|
||||
try:
|
||||
opened = webbrowser.open(deployment_url)
|
||||
except Exception:
|
||||
opened = False
|
||||
|
||||
if not opened:
|
||||
console.print(
|
||||
"Could not open the deployment page automatically.",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
def deploy(self, uuid: str | None = None, skip_validate: bool = False) -> None:
|
||||
"""
|
||||
Deploy a crew using either UUID or project name.
|
||||
@@ -438,6 +499,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
console.print("crewai deploy push")
|
||||
console.print(" or")
|
||||
console.print(f"crewai deploy push --uuid {json_response['uuid']}")
|
||||
self._open_deployment_page(json_response)
|
||||
|
||||
def list_crews(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ from crewai_cli.utils import (
|
||||
is_dmn_mode_enabled,
|
||||
read_toml,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_version
|
||||
from crewai_cli.version import get_crewai_tools_dependency, get_crewai_version
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -32,12 +32,12 @@ if TYPE_CHECKING:
|
||||
_INPUT_PLACEHOLDER_RE = re.compile(r"(?<!{){([A-Za-z_][A-Za-z0-9_\-]*)}(?!})")
|
||||
_CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV = "CREWAI_CLI_RUNNER_PACKAGE_DIR"
|
||||
_CREWAI_RUNNER_SOURCE_DIR_ENV = "CREWAI_RUNNER_SOURCE_DIR"
|
||||
_FULL_CREWAI_INSTALL_MESSAGE = """\
|
||||
_FULL_CREWAI_INSTALL_MESSAGE = f"""\
|
||||
CrewAI CLI is installed without the `crewai` package required to run crews.
|
||||
|
||||
Install the full CrewAI prerelease package:
|
||||
Install the full CrewAI package:
|
||||
|
||||
uv tool install --force --prerelease=allow 'crewai[tools]==1.14.8a1'
|
||||
uv tool install --force '{get_crewai_tools_dependency()}'
|
||||
|
||||
The quotes are required in zsh so `crewai[tools]` is not treated as a glob.
|
||||
"""
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.15.0"
|
||||
"{{crewai_tools_dependency}}"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -23,6 +23,7 @@ from crewai_cli.utils import (
|
||||
tree_copy,
|
||||
tree_find_and_replace,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
console = Console()
|
||||
@@ -81,6 +82,9 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
tree_copy(template_dir, project_root)
|
||||
tree_find_and_replace(project_root, "{{folder_name}}", folder_name)
|
||||
tree_find_and_replace(project_root, "{{class_name}}", class_name)
|
||||
tree_find_and_replace(
|
||||
project_root, "{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
|
||||
agents_md_src = Path(__file__).parent.parent / "templates" / "AGENTS.md"
|
||||
if agents_md_src.exists():
|
||||
|
||||
@@ -19,6 +19,8 @@ from crewai_core.tool_credentials import (
|
||||
)
|
||||
from rich.console import Console
|
||||
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
__all__ = [
|
||||
"build_env_with_all_tool_credentials",
|
||||
@@ -73,6 +75,9 @@ def copy_template(
|
||||
content = content.replace("{{name}}", name)
|
||||
content = content.replace("{{crew_name}}", class_name)
|
||||
content = content.replace("{{folder_name}}", folder_name)
|
||||
content = content.replace(
|
||||
"{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
||||
)
|
||||
|
||||
with open(dst, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
@@ -13,10 +13,26 @@ from crewai_core.version import (
|
||||
is_current_version_yanked as is_current_version_yanked,
|
||||
is_newer_version_available as is_newer_version_available,
|
||||
)
|
||||
from packaging.version import Version
|
||||
|
||||
from crewai_cli import __version__ as _crewai_cli_version
|
||||
|
||||
|
||||
def get_crewai_dependency_range(current_version: str | None = None) -> str:
|
||||
"""Return the supported CrewAI dependency range for generated projects."""
|
||||
parsed_version = Version(current_version or _crewai_cli_version)
|
||||
return f">={parsed_version},<{parsed_version.major + 1}.0.0"
|
||||
|
||||
|
||||
def get_crewai_tools_dependency(current_version: str | None = None) -> str:
|
||||
"""Return the generated-project dependency for CrewAI with tools."""
|
||||
return f"crewai[tools]{get_crewai_dependency_range(current_version)}"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"check_version",
|
||||
"get_crewai_dependency_range",
|
||||
"get_crewai_tools_dependency",
|
||||
"get_crewai_version",
|
||||
"get_latest_version_from_pypi",
|
||||
"is_current_version_yanked",
|
||||
|
||||
@@ -176,7 +176,7 @@ def test_create_project_zip_keeps_json_project_root_shape(tmp_path: Path):
|
||||
[project]
|
||||
name = "json_crew"
|
||||
version = "0.1.0"
|
||||
dependencies = ["crewai[tools]==1.14.8a1"]
|
||||
dependencies = ["crewai[tools]>=1.15.0,<2.0.0"]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
@@ -167,6 +167,36 @@ def test_prepare_project_for_deploy_creates_missing_lock_after_validation(
|
||||
assert validators == []
|
||||
|
||||
|
||||
def test_deployment_page_url_prefers_deployment_id():
|
||||
assert (
|
||||
deploy_main._deployment_page_url(
|
||||
"https://app.crewai.com",
|
||||
{"uuid": "crew-uuid", "deployment_id": 128687},
|
||||
)
|
||||
== "https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_page_url_prefers_nested_deployment_id_over_crew_uuid():
|
||||
assert (
|
||||
deploy_main._deployment_page_url(
|
||||
"https://app.crewai.com",
|
||||
{"uuid": "crew-uuid", "deployment": {"deployment_id": 128687}},
|
||||
)
|
||||
== "https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
|
||||
def test_deployment_page_url_falls_back_to_nested_uuid():
|
||||
assert (
|
||||
deploy_main._deployment_page_url(
|
||||
"https://app.crewai.com/",
|
||||
{"deployment": {"uuid": "deployment-uuid"}},
|
||||
)
|
||||
== "https://app.crewai.com/crewai_plus/deployments/deployment-uuid"
|
||||
)
|
||||
|
||||
|
||||
class TestDeployCommand(unittest.TestCase):
|
||||
@patch("crewai_cli.command.get_auth_token")
|
||||
@patch("crewai_cli.deploy.main.get_project_name")
|
||||
@@ -186,6 +216,12 @@ class TestDeployCommand(unittest.TestCase):
|
||||
|
||||
self.deploy_command = deploy_main.DeployCommand()
|
||||
self.mock_client = self.deploy_command.plus_api_client
|
||||
self.mock_client.base_url = "https://app.crewai.com"
|
||||
self.mock_browser_open_patcher = patch(
|
||||
"crewai_cli.deploy.main.webbrowser.open"
|
||||
)
|
||||
self.mock_browser_open = self.mock_browser_open_patcher.start()
|
||||
self.addCleanup(self.mock_browser_open_patcher.stop)
|
||||
|
||||
def test_init_success(self):
|
||||
self.assertEqual(self.deploy_command.project_name, "test_project")
|
||||
@@ -272,11 +308,50 @@ class TestDeployCommand(unittest.TestCase):
|
||||
def test_display_deployment_info(self):
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command._display_deployment_info(
|
||||
{"uuid": "test-uuid", "status": "deployed"}
|
||||
{"uuid": "test-uuid", "id": 128687, "status": "deployed"}
|
||||
)
|
||||
self.assertIn("Deploying the crew...", fake_out.getvalue())
|
||||
self.assertIn("test-uuid", fake_out.getvalue())
|
||||
self.assertIn("deployed", fake_out.getvalue())
|
||||
self.assertIn(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687",
|
||||
fake_out.getvalue(),
|
||||
)
|
||||
self.mock_browser_open.assert_called_once_with(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
def test_display_deployment_info_warns_when_browser_open_returns_false(self):
|
||||
self.mock_browser_open.return_value = False
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command._display_deployment_info(
|
||||
{"uuid": "test-uuid", "id": 128687, "status": "deployed"}
|
||||
)
|
||||
self.assertIn(
|
||||
"Could not open the deployment page automatically.",
|
||||
fake_out.getvalue(),
|
||||
)
|
||||
|
||||
self.mock_browser_open.assert_called_once_with(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
def test_display_deployment_info_warns_when_browser_open_raises(self):
|
||||
self.mock_browser_open.side_effect = RuntimeError("no browser")
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command._display_deployment_info(
|
||||
{"uuid": "test-uuid", "id": 128687, "status": "deployed"}
|
||||
)
|
||||
self.assertIn(
|
||||
"Could not open the deployment page automatically.",
|
||||
fake_out.getvalue(),
|
||||
)
|
||||
|
||||
self.mock_browser_open.assert_called_once_with(
|
||||
"https://app.crewai.com/crewai_plus/deployments/128687"
|
||||
)
|
||||
|
||||
def test_display_logs(self):
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
|
||||
@@ -735,8 +735,9 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
|
||||
|
||||
pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text())
|
||||
dependency = pyproject["project"]["dependencies"][0]
|
||||
assert dependency == "crewai[tools]==1.14.8a1"
|
||||
assert Version("1.14.8a1") in Requirement(dependency).specifier
|
||||
assert dependency == "crewai[tools]>=1.15.0,<2.0.0"
|
||||
assert Version("1.15.0") in Requirement(dependency).specifier
|
||||
assert Version("2.0.0") not in Requirement(dependency).specifier
|
||||
assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"][
|
||||
"only-include"
|
||||
] == ["agents", "crew.jsonc", "tools", "knowledge", "skills"]
|
||||
|
||||
@@ -25,10 +25,7 @@ def test_missing_crewai_package_shows_full_install_hint(monkeypatch):
|
||||
|
||||
message = exc_info.value.message
|
||||
assert "CrewAI CLI is installed without the `crewai` package" in message
|
||||
assert (
|
||||
"uv tool install --force --prerelease=allow 'crewai[tools]==1.14.8a1'"
|
||||
in message
|
||||
)
|
||||
assert "uv tool install --force 'crewai[tools]>=1.15.0,<2.0.0'" in message
|
||||
assert "quotes are required in zsh" in message
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai_cli.version import get_crewai_version as _get_ver
|
||||
from crewai_cli.version import (
|
||||
get_crewai_dependency_range,
|
||||
get_crewai_tools_dependency,
|
||||
get_crewai_version,
|
||||
get_latest_version_from_pypi,
|
||||
is_current_version_yanked,
|
||||
@@ -31,6 +33,11 @@ def test_dynamic_versioning_consistency() -> None:
|
||||
assert len(package_version.strip()) > 0
|
||||
|
||||
|
||||
def test_generated_project_dependency_uses_next_major_upper_bound() -> None:
|
||||
assert get_crewai_dependency_range("1.15.0") == ">=1.15.0,<2.0.0"
|
||||
assert get_crewai_tools_dependency("1.15.0") == "crewai[tools]>=1.15.0,<2.0.0"
|
||||
|
||||
|
||||
class TestVersionChecking:
|
||||
"""Test version checking utilities."""
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ def test_create_success(mock_subprocess, capsys, tool_command):
|
||||
)
|
||||
assert os.path.isfile(os.path.join("test_tool", "src", "test_tool", "tool.py"))
|
||||
|
||||
with open(os.path.join("test_tool", "pyproject.toml"), "r") as f:
|
||||
content = f.read()
|
||||
assert '"crewai[tools]>=1.15.0,<2.0.0"' in content
|
||||
|
||||
with open(os.path.join("test_tool", "src", "test_tool", "tool.py"), "r") as f:
|
||||
content = f.read()
|
||||
assert "class TestTool" in content
|
||||
|
||||
@@ -8,6 +8,7 @@ import requests
|
||||
|
||||
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
|
||||
from crewai_tools.rag.source_content import SourceContent
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
class DocsSiteLoader(BaseLoader):
|
||||
@@ -26,7 +27,7 @@ class DocsSiteLoader(BaseLoader):
|
||||
docs_url = source.source
|
||||
|
||||
try:
|
||||
response = requests.get(docs_url, timeout=30)
|
||||
response = safe_get(docs_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
raise ValueError(
|
||||
|
||||
@@ -2,10 +2,9 @@ import os
|
||||
import tempfile
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
|
||||
from crewai_tools.rag.source_content import SourceContent
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
class DOCXLoader(BaseLoader):
|
||||
@@ -43,7 +42,7 @@ class DOCXLoader(BaseLoader):
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response = safe_get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Create temporary file to save the DOCX content
|
||||
|
||||
@@ -6,10 +6,9 @@ import tempfile
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
|
||||
from crewai_tools.rag.source_content import SourceContent
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
class PDFLoader(BaseLoader):
|
||||
@@ -47,7 +46,7 @@ class PDFLoader(BaseLoader):
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response = safe_get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file:
|
||||
|
||||
@@ -23,7 +23,7 @@ def load_from_url(
|
||||
Raises:
|
||||
ValueError: If there's an error fetching the URL
|
||||
"""
|
||||
import requests
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
headers = kwargs.get(
|
||||
"headers",
|
||||
@@ -34,7 +34,7 @@ def load_from_url(
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response = safe_get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
except Exception as e:
|
||||
|
||||
@@ -2,10 +2,10 @@ import re
|
||||
from typing import Any, Final
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
|
||||
from crewai_tools.rag.base_loader import BaseLoader, LoaderResult
|
||||
from crewai_tools.rag.source_content import SourceContent
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
_SPACES_PATTERN: Final[re.Pattern[str]] = re.compile(r"[ \t]+")
|
||||
@@ -25,7 +25,7 @@ class WebPageLoader(BaseLoader):
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=15, headers=headers)
|
||||
response = safe_get(url, timeout=15, headers=headers)
|
||||
response.encoding = response.apparent_encoding
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
|
||||
88
lib/crewai-tools/src/crewai_tools/security/safe_requests.py
Normal file
88
lib/crewai-tools/src/crewai_tools/security/safe_requests.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""HTTP helpers that preserve crewai-tools URL safety checks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from crewai_tools.security.safe_path import validate_url
|
||||
|
||||
|
||||
_REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
|
||||
_SENSITIVE_HEADER_NAMES = {
|
||||
"authorization",
|
||||
"cookie",
|
||||
"proxy-authorization",
|
||||
"x-api-key",
|
||||
}
|
||||
_SENSITIVE_HEADER_FRAGMENTS = ("api-key", "apikey", "secret", "token")
|
||||
|
||||
|
||||
def _same_origin(previous_url: str, next_url: str) -> bool:
|
||||
previous = urlparse(previous_url)
|
||||
next_ = urlparse(next_url)
|
||||
return (previous.scheme, previous.netloc) == (next_.scheme, next_.netloc)
|
||||
|
||||
|
||||
def _is_sensitive_header(header: str) -> bool:
|
||||
normalized = header.lower()
|
||||
return (
|
||||
normalized in _SENSITIVE_HEADER_NAMES
|
||||
or normalized.startswith("authorization-")
|
||||
or any(fragment in normalized for fragment in _SENSITIVE_HEADER_FRAGMENTS)
|
||||
)
|
||||
|
||||
|
||||
def _strip_cross_origin_credentials(request_kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
sanitized = {**request_kwargs}
|
||||
headers = sanitized.get("headers")
|
||||
if headers:
|
||||
sanitized["headers"] = {
|
||||
key: value
|
||||
for key, value in headers.items()
|
||||
if not _is_sensitive_header(str(key))
|
||||
}
|
||||
sanitized.pop("cookies", None)
|
||||
return sanitized
|
||||
|
||||
|
||||
def safe_get(url: str, *, max_redirects: int = 10, **kwargs: Any) -> requests.Response:
|
||||
"""GET a URL while validating each redirect target before following it."""
|
||||
current_url = validate_url(url)
|
||||
request_kwargs = {**kwargs, "allow_redirects": False}
|
||||
timeout = request_kwargs.pop("timeout", 30)
|
||||
history: list[requests.Response] = []
|
||||
redirects_followed = 0
|
||||
|
||||
while True:
|
||||
response = requests.get(current_url, timeout=timeout, **request_kwargs)
|
||||
if (
|
||||
response.status_code not in _REDIRECT_STATUS_CODES
|
||||
or "Location" not in response.headers
|
||||
):
|
||||
response.history = history
|
||||
return response
|
||||
|
||||
if redirects_followed >= max_redirects:
|
||||
response.close()
|
||||
raise ValueError(f"Too many redirects while fetching URL: {url}")
|
||||
|
||||
location = response.headers.get("Location")
|
||||
if not location:
|
||||
response.history = history
|
||||
return response
|
||||
|
||||
try:
|
||||
redirect_url = validate_url(urljoin(response.url, location))
|
||||
except ValueError:
|
||||
response.close()
|
||||
raise
|
||||
|
||||
if not _same_origin(current_url, redirect_url):
|
||||
request_kwargs = _strip_cross_origin_credentials(request_kwargs)
|
||||
|
||||
history.append(response)
|
||||
current_url = redirect_url
|
||||
redirects_followed += 1
|
||||
@@ -3,9 +3,8 @@ from typing import Any
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
from pydantic import BaseModel, Field
|
||||
import requests
|
||||
|
||||
from crewai_tools.security.safe_path import validate_url
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
try:
|
||||
@@ -83,8 +82,7 @@ class ScrapeElementFromWebsiteTool(BaseTool):
|
||||
if website_url is None or css_element is None:
|
||||
raise ValueError("Both website_url and css_element must be provided.")
|
||||
|
||||
website_url = validate_url(website_url)
|
||||
page = requests.get(
|
||||
page = safe_get(
|
||||
website_url,
|
||||
headers=self.headers,
|
||||
cookies=self.cookies if self.cookies else {},
|
||||
|
||||
@@ -3,9 +3,8 @@ import re
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
import requests
|
||||
|
||||
from crewai_tools.security.safe_path import validate_url
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
try:
|
||||
@@ -75,8 +74,7 @@ class ScrapeWebsiteTool(BaseTool):
|
||||
if website_url is None:
|
||||
raise ValueError("Website URL must be provided.")
|
||||
|
||||
website_url = validate_url(website_url)
|
||||
page = requests.get(
|
||||
page = safe_get(
|
||||
website_url,
|
||||
timeout=15,
|
||||
headers=self.headers,
|
||||
|
||||
28
lib/crewai-tools/tests/rag/conftest.py
Normal file
28
lib/crewai-tools/tests/rag/conftest.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def public_example_dns(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
original_getaddrinfo = socket.getaddrinfo
|
||||
|
||||
def fake_getaddrinfo(
|
||||
host: str, port: int, *args: Any, **kwargs: Any
|
||||
) -> list[tuple[Any, ...]]:
|
||||
if host in {"example.com", "api.example.com"}:
|
||||
return [
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
6,
|
||||
"",
|
||||
("93.184.216.34", port),
|
||||
)
|
||||
]
|
||||
return original_getaddrinfo(host, port, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
|
||||
177
lib/crewai-tools/tests/utilities/test_safe_requests.py
Normal file
177
lib/crewai-tools/tests/utilities/test_safe_requests.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Tests for redirect-aware safe HTTP helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from crewai_tools.security.safe_requests import safe_get
|
||||
|
||||
|
||||
def _response(url: str, status_code: int, *, location: str | None = None) -> requests.Response:
|
||||
response = requests.Response()
|
||||
response.status_code = status_code
|
||||
response.url = url
|
||||
response._content = b"ok"
|
||||
response.raw = BytesIO()
|
||||
if location is not None:
|
||||
response.headers["Location"] = location
|
||||
return response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def public_dns(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
original_getaddrinfo = socket.getaddrinfo
|
||||
|
||||
def fake_getaddrinfo(
|
||||
host: str, port: int, *args: Any, **kwargs: Any
|
||||
) -> list[tuple[Any, ...]]:
|
||||
if host in {"public.example", "safe.example"}:
|
||||
return [
|
||||
(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM,
|
||||
6,
|
||||
"",
|
||||
("93.184.216.34", port),
|
||||
)
|
||||
]
|
||||
return original_getaddrinfo(host, port, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
|
||||
|
||||
|
||||
def test_safe_get_blocks_direct_internal_url() -> None:
|
||||
with pytest.raises(ValueError, match="private/reserved IP"):
|
||||
safe_get("http://127.0.0.1/admin", timeout=15)
|
||||
|
||||
|
||||
def _mock_get(monkeypatch: pytest.MonkeyPatch, get_response: Any) -> None:
|
||||
monkeypatch.setattr(
|
||||
"crewai_tools.security.safe_requests.requests.get",
|
||||
get_response,
|
||||
)
|
||||
|
||||
|
||||
def test_safe_get_blocks_redirect_to_internal_url(
|
||||
monkeypatch: pytest.MonkeyPatch, public_dns: None
|
||||
) -> None:
|
||||
requested_urls: list[str] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: Any) -> requests.Response:
|
||||
requested_urls.append(url)
|
||||
assert kwargs["allow_redirects"] is False
|
||||
return _response(url, 302, location="http://127.0.0.1/admin")
|
||||
|
||||
_mock_get(monkeypatch, fake_get)
|
||||
|
||||
with pytest.raises(ValueError, match="private/reserved IP"):
|
||||
safe_get("http://public.example/start", timeout=15)
|
||||
|
||||
assert requested_urls == ["http://public.example/start"]
|
||||
|
||||
|
||||
def test_safe_get_follows_safe_relative_redirect(
|
||||
monkeypatch: pytest.MonkeyPatch, public_dns: None
|
||||
) -> None:
|
||||
requested_urls: list[str] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: Any) -> requests.Response:
|
||||
requested_urls.append(url)
|
||||
assert kwargs["allow_redirects"] is False
|
||||
if url == "http://public.example/start":
|
||||
return _response(url, 302, location="/final")
|
||||
return _response(url, 200)
|
||||
|
||||
_mock_get(monkeypatch, fake_get)
|
||||
|
||||
response = safe_get("http://public.example/start", timeout=15)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.url == "http://public.example/final"
|
||||
assert requested_urls == [
|
||||
"http://public.example/start",
|
||||
"http://public.example/final",
|
||||
]
|
||||
assert len(response.history) == 1
|
||||
|
||||
|
||||
def test_safe_get_fails_closed_after_too_many_redirects(
|
||||
monkeypatch: pytest.MonkeyPatch, public_dns: None
|
||||
) -> None:
|
||||
def fake_get(url: str, **kwargs: Any) -> requests.Response:
|
||||
return _response(url, 302, location="http://safe.example/again")
|
||||
|
||||
_mock_get(monkeypatch, fake_get)
|
||||
|
||||
with pytest.raises(ValueError, match="Too many redirects"):
|
||||
safe_get("http://public.example/start", max_redirects=1, timeout=15)
|
||||
|
||||
|
||||
def test_safe_get_strips_credentials_on_cross_origin_redirect(
|
||||
monkeypatch: pytest.MonkeyPatch, public_dns: None
|
||||
) -> None:
|
||||
requests_made: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: Any) -> requests.Response:
|
||||
requests_made.append((url, kwargs))
|
||||
if url == "http://public.example/start":
|
||||
return _response(url, 302, location="http://safe.example/final")
|
||||
return _response(url, 200)
|
||||
|
||||
_mock_get(monkeypatch, fake_get)
|
||||
|
||||
response = safe_get(
|
||||
"http://public.example/start",
|
||||
timeout=15,
|
||||
headers={
|
||||
"Authorization": "Bearer token",
|
||||
"Authorization-Custom": "secret token",
|
||||
"Cookie": "session=abc",
|
||||
"X-API-Key": "api key",
|
||||
"X-CrewAI-Token": "crewai token",
|
||||
"User-Agent": "crewai-test",
|
||||
},
|
||||
cookies={"session": "abc"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert requests_made[0][1]["headers"] == {
|
||||
"Authorization": "Bearer token",
|
||||
"Authorization-Custom": "secret token",
|
||||
"Cookie": "session=abc",
|
||||
"X-API-Key": "api key",
|
||||
"X-CrewAI-Token": "crewai token",
|
||||
"User-Agent": "crewai-test",
|
||||
}
|
||||
assert requests_made[0][1]["cookies"] == {"session": "abc"}
|
||||
assert requests_made[1][1]["headers"] == {"User-Agent": "crewai-test"}
|
||||
assert "cookies" not in requests_made[1][1]
|
||||
|
||||
|
||||
def test_safe_get_preserves_credentials_on_same_origin_redirect(
|
||||
monkeypatch: pytest.MonkeyPatch, public_dns: None
|
||||
) -> None:
|
||||
requests_made: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: Any) -> requests.Response:
|
||||
requests_made.append((url, kwargs))
|
||||
if url == "http://public.example/start":
|
||||
return _response(url, 302, location="/final")
|
||||
return _response(url, 200)
|
||||
|
||||
_mock_get(monkeypatch, fake_get)
|
||||
|
||||
safe_get(
|
||||
"http://public.example/start",
|
||||
timeout=15,
|
||||
headers={"Authorization": "Bearer token"},
|
||||
cookies={"session": "abc"},
|
||||
)
|
||||
|
||||
assert requests_made[1][1]["headers"] == {"Authorization": "Bearer token"}
|
||||
assert requests_made[1][1]["cookies"] == {"session": "abc"}
|
||||
Reference in New Issue
Block a user