mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-09 20:42:35 +00:00
Compare commits
2 Commits
devin/1774
...
docs/file-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8ba5b823a | ||
|
|
5a850a708b |
2
.github/workflows/docs-broken-links.yml
vendored
2
.github/workflows/docs-broken-links.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "latest"
|
||||
|
||||
- name: Install Mintlify CLI
|
||||
run: npm i -g mintlify
|
||||
|
||||
@@ -4,86 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="27 مارس 2026">
|
||||
## v1.13.0rc1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.13.0a2
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 مارس 2026">
|
||||
## v1.13.0a2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- تحديث تلقائي لمستودع اختبار النشر أثناء الإصدار
|
||||
- تحسين مرونة إصدار المؤسسات وتجربة المستخدم
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار للإصدار v1.13.0a1
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 مارس 2026">
|
||||
## v1.13.0a1
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح الروابط المعطلة في سير العمل الوثائقي عن طريق تثبيت Node على LTS 22
|
||||
- مسح ذاكرة التخزين المؤقت لـ uv للحزم المنشورة حديثًا في الإصدار المؤسسي
|
||||
|
||||
### الوثائق
|
||||
- إضافة مصفوفة شاملة لأذونات RBAC ودليل النشر
|
||||
- تحديث سجل التغييرات والإصدار للإصدار v1.12.2
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.2
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة مرحلة إصدار المؤسسات إلى إصدار أدوات المطورين
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- الحفاظ على قيمة إرجاع الطريقة كإخراج تدفق لـ @human_feedback مع emit
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.12.1
|
||||
- مراجعة سياسة الأمان وتعليمات الإبلاغ
|
||||
|
||||
## المساهمون
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 مارس 2026">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
1863
docs/docs.json
1863
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,86 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 27, 2026">
|
||||
## v1.13.0rc1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 27, 2026">
|
||||
## v1.13.0a2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Auto-update deployment test repo during release
|
||||
- Improve enterprise release resilience and UX
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.13.0a1
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 27, 2026">
|
||||
## v1.13.0a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Fix broken links in documentation workflow by pinning Node to LTS 22
|
||||
- Bust the uv cache for freshly published packages in enterprise release
|
||||
|
||||
### Documentation
|
||||
- Add comprehensive RBAC permissions matrix and deployment guide
|
||||
- Update changelog and version for v1.12.2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add enterprise release phase to devtools release
|
||||
|
||||
### Bug Fixes
|
||||
- Preserve method return value as flow output for @human_feedback with emit
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.12.1
|
||||
- Revise security policy and reporting instructions
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 25, 2026">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
@@ -134,6 +134,29 @@ result = flow.kickoff(
|
||||
)
|
||||
```
|
||||
|
||||
You can also define file types directly in your flow state for structured file handling:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai_files import ImageFile, PDFFile
|
||||
|
||||
class DocumentState(BaseModel):
|
||||
document: PDFFile
|
||||
cover_image: ImageFile
|
||||
title: str = ""
|
||||
|
||||
class DocumentFlow(Flow[DocumentState]):
|
||||
@start()
|
||||
def process(self):
|
||||
content = self.state.document.read()
|
||||
return {"processed": True}
|
||||
```
|
||||
|
||||
<Note type="info" title="CrewAI Platform Integration">
|
||||
When deploying flows to the CrewAI Platform (AMP), file fields in your state automatically render as file upload dropzones in the UI. For API usage, you can pass URL strings directly and Pydantic coerces them to file objects automatically. See [Flows - File Inputs](/en/concepts/flows#file-inputs) for details.
|
||||
</Note>
|
||||
|
||||
### With Standalone Agents
|
||||
|
||||
Pass files directly to agent kickoff:
|
||||
|
||||
@@ -341,6 +341,69 @@ flow.kickoff()
|
||||
|
||||
By providing both unstructured and structured state management options, CrewAI Flows empowers developers to build AI workflows that are both flexible and robust, catering to a wide range of application requirements.
|
||||
|
||||
## File Inputs
|
||||
|
||||
Flows support file inputs through the `crewai-files` package, enabling you to build workflows that process images, PDFs, and other file types. When you use file types like `ImageFile` or `PDFFile` in your flow state, they integrate seamlessly with both local development and the CrewAI Platform.
|
||||
|
||||
<Note type="info" title="Optional Dependency">
|
||||
File support requires the optional `crewai-files` package. Install it with:
|
||||
|
||||
```bash
|
||||
uv add 'crewai[file-processing]'
|
||||
```
|
||||
</Note>
|
||||
|
||||
### Using File Types in Flow State
|
||||
|
||||
You can include file types directly in your structured flow state:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai_files import ImageFile, PDFFile
|
||||
|
||||
class DocumentProcessingState(BaseModel):
|
||||
document: PDFFile # Renders as file upload in CrewAI Platform
|
||||
cover_image: ImageFile # Renders as image upload
|
||||
title: str = "" # Renders as text input
|
||||
|
||||
class DocumentFlow(Flow[DocumentProcessingState]):
|
||||
@start()
|
||||
def process_document(self):
|
||||
# Access the file - works with URLs, paths, or uploaded files
|
||||
content = self.state.document.read()
|
||||
# Or pass to an agent with VisionTool, etc.
|
||||
return {"processed": True}
|
||||
```
|
||||
|
||||
### CrewAI Platform Integration
|
||||
|
||||
When you deploy a flow to the CrewAI Platform (AMP), file fields in your state automatically render as file upload dropzones in the UI. This makes it easy to build user-facing applications that accept file uploads without any additional frontend work.
|
||||
|
||||
| State Field Type | Platform UI Rendering |
|
||||
|:-----------------|:----------------------|
|
||||
| `ImageFile` | Image upload dropzone |
|
||||
| `PDFFile` | PDF upload dropzone |
|
||||
| `AudioFile` | Audio upload dropzone |
|
||||
| `VideoFile` | Video upload dropzone |
|
||||
| `TextFile` | Text file upload dropzone |
|
||||
| `str`, `int`, etc. | Standard form inputs |
|
||||
|
||||
### API Usage
|
||||
|
||||
When calling your flow via API, you can pass URL strings directly for file fields. Pydantic automatically coerces URLs into the appropriate file type:
|
||||
|
||||
```python
|
||||
# API request body - URLs are automatically converted to file objects
|
||||
{
|
||||
"document": "https://example.com/report.pdf",
|
||||
"cover_image": "https://example.com/cover.png",
|
||||
"title": "Q4 Report"
|
||||
}
|
||||
```
|
||||
|
||||
For more details on file types, sources, and provider support, see the [Files documentation](/en/concepts/files).
|
||||
|
||||
## Flow Persistence
|
||||
|
||||
The @persist decorator enables automatic state persistence in CrewAI Flows, allowing you to maintain flow state across restarts or different workflow executions. This decorator can be applied at either the class level or method level, providing flexibility in how you manage state persistence.
|
||||
|
||||
@@ -7,13 +7,11 @@ mode: "wide"
|
||||
|
||||
## Overview
|
||||
|
||||
RBAC in CrewAI AMP enables secure, scalable access management through two layers:
|
||||
|
||||
1. **Feature permissions** — control what each role can do across the platform (manage, read, or no access)
|
||||
2. **Entity-level permissions** — fine-grained access on individual automations, environment variables, LLM connections, and Git repositories
|
||||
RBAC in CrewAI AMP enables secure, scalable access management through a combination of organization‑level roles and automation‑level visibility controls.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/users_and_roles.png" alt="RBAC overview in CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
## Users and Roles
|
||||
@@ -41,13 +39,6 @@ You can configure users and roles in Settings → Roles.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Predefined Roles
|
||||
|
||||
| Role | Description |
|
||||
| :--------- | :-------------------------------------------------------------------------- |
|
||||
| **Owner** | Full access to all features and settings. Cannot be restricted. |
|
||||
| **Member** | Read access to most features, manage access to Studio projects. Cannot modify organization or default settings. |
|
||||
|
||||
### Configuration summary
|
||||
|
||||
| Area | Where to configure | Options |
|
||||
@@ -55,80 +46,23 @@ You can configure users and roles in Settings → Roles.
|
||||
| Users & Roles | Settings → Roles | Predefined: Owner, Member; Custom roles |
|
||||
| Automation visibility | Automation → Settings → Visibility | Private; Whitelist users/roles |
|
||||
|
||||
---
|
||||
## Automation‑level Access Control
|
||||
|
||||
## Feature Permissions Matrix
|
||||
In addition to organization‑wide roles, CrewAI Automations support fine‑grained visibility settings that let you restrict access to specific automations by user or role.
|
||||
|
||||
Every role has a permission level for each feature area. The three levels are:
|
||||
|
||||
- **Manage** — full read/write access (create, edit, delete)
|
||||
- **Read** — view-only access
|
||||
- **No access** — feature is hidden/inaccessible
|
||||
|
||||
| Feature | Owner | Member (default) | Description |
|
||||
| :------------------------ | :------ | :--------------- | :-------------------------------------------------------------- |
|
||||
| `usage_dashboards` | Manage | Read | View usage metrics and analytics |
|
||||
| `crews_dashboards` | Manage | Read | View deployment dashboards, access automation details |
|
||||
| `invitations` | Manage | Read | Invite new members to the organization |
|
||||
| `training_ui` | Manage | Read | Access training/fine-tuning interfaces |
|
||||
| `tools` | Manage | Read | Create and manage tools |
|
||||
| `agents` | Manage | Read | Create and manage agents |
|
||||
| `environment_variables` | Manage | Read | Create and manage environment variables |
|
||||
| `llm_connections` | Manage | Read | Configure LLM provider connections |
|
||||
| `default_settings` | Manage | No access | Modify organization-wide default settings |
|
||||
| `organization_settings` | Manage | No access | Manage billing, plans, and organization configuration |
|
||||
| `studio_projects` | Manage | Manage | Create and edit projects in Studio |
|
||||
|
||||
<Tip>
|
||||
When creating a custom role, you can set each feature independently to **Manage**, **Read**, or **No access** to match your team's needs.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Deploying from GitHub or Zip
|
||||
|
||||
One of the most common RBAC questions is: _"What permissions does a team member need to deploy?"_
|
||||
|
||||
### Deploy from GitHub
|
||||
|
||||
To deploy an automation from a GitHub repository, a user needs:
|
||||
|
||||
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard where deployments are created
|
||||
2. **Git repository access** (if entity-level RBAC for Git repositories is enabled): the user's role must be granted access to the specific Git repository via entity-level permissions
|
||||
3. **`studio_projects`: `Manage`** — if building the crew in Studio before deploying
|
||||
|
||||
### Deploy from Zip
|
||||
|
||||
To deploy an automation from a Zip file upload, a user needs:
|
||||
|
||||
1. **`crews_dashboards`**: at least `Read` — required to access the automations dashboard
|
||||
2. **Zip deployments enabled**: the organization must not have disabled zip deployments in organization settings
|
||||
|
||||
### Quick Reference: Minimum Permissions for Deployment
|
||||
|
||||
| Action | Required feature permissions | Additional requirements |
|
||||
| :------------------- | :------------------------------------ | :----------------------------------------------- |
|
||||
| Deploy from GitHub | `crews_dashboards: Read` | Git repo entity access (if Git RBAC is enabled) |
|
||||
| Deploy from Zip | `crews_dashboards: Read` | Zip deployments must be enabled at the org level |
|
||||
| Build in Studio | `studio_projects: Manage` | — |
|
||||
| Configure LLM keys | `llm_connections: Manage` | — |
|
||||
| Set environment vars | `environment_variables: Manage` | Entity-level access (if entity RBAC is enabled) |
|
||||
|
||||
---
|
||||
|
||||
## Automation‑level Access Control (Entity Permissions)
|
||||
|
||||
In addition to organization‑wide roles, CrewAI supports fine‑grained entity-level permissions that restrict access to individual resources.
|
||||
|
||||
### Automation Visibility
|
||||
|
||||
Automations support visibility settings that restrict access by user or role. This is useful for:
|
||||
This is useful for:
|
||||
|
||||
- Keeping sensitive or experimental automations private
|
||||
- Managing visibility across large teams or external collaborators
|
||||
- Testing automations in isolated contexts
|
||||
|
||||
Deployments can be configured as private, meaning only whitelisted users and roles will be able to interact with them.
|
||||
Deployments can be configured as private, meaning only whitelisted users and roles will be able to:
|
||||
|
||||
- View the deployment
|
||||
- Run it or interact with its API
|
||||
- Access its logs, metrics, and settings
|
||||
|
||||
The organization owner always has access, regardless of visibility settings.
|
||||
|
||||
You can configure automation‑level access control in Automation → Settings → Visibility tab.
|
||||
|
||||
@@ -165,92 +99,9 @@ You can configure automation‑level access control in Automation → Settings
|
||||
|
||||
<Frame>
|
||||
<img src="/images/enterprise/visibility.png" alt="Automation Visibility settings in CrewAI AMP" />
|
||||
|
||||
</Frame>
|
||||
|
||||
### Deployment Permission Types
|
||||
|
||||
When granting entity-level access to a specific automation, you can assign these permission types:
|
||||
|
||||
| Permission | What it allows |
|
||||
| :------------------- | :-------------------------------------------------- |
|
||||
| `run` | Execute the automation and use its API |
|
||||
| `traces` | View execution traces and logs |
|
||||
| `manage_settings` | Edit, redeploy, rollback, or delete the automation |
|
||||
| `human_in_the_loop` | Respond to human-in-the-loop (HITL) requests |
|
||||
| `full_access` | All of the above |
|
||||
|
||||
### Entity-level RBAC for Other Resources
|
||||
|
||||
When entity-level RBAC is enabled, access to these resources can also be controlled per user or role:
|
||||
|
||||
| Resource | Controlled by | Description |
|
||||
| :--------------------- | :------------------------------- | :---------------------------------------------------- |
|
||||
| Environment variables | Entity RBAC feature flag | Restrict which roles/users can view or manage specific env vars |
|
||||
| LLM connections | Entity RBAC feature flag | Restrict access to specific LLM provider configurations |
|
||||
| Git repositories | Git repositories RBAC org setting | Restrict which roles/users can access specific connected repos |
|
||||
|
||||
---
|
||||
|
||||
## Common Role Patterns
|
||||
|
||||
While CrewAI ships with Owner and Member roles, most teams benefit from creating custom roles. Here are common patterns:
|
||||
|
||||
### Developer Role
|
||||
|
||||
A role for team members who build and deploy automations but don't manage organization settings.
|
||||
|
||||
| Feature | Permission |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Read |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Manage |
|
||||
| `agents` | Manage |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Read |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Manage |
|
||||
|
||||
### Viewer / Stakeholder Role
|
||||
|
||||
A role for non-technical stakeholders who need to monitor automations and view results.
|
||||
|
||||
| Feature | Permission |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Read |
|
||||
| `crews_dashboards` | Read |
|
||||
| `invitations` | No access |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | No access |
|
||||
| `llm_connections` | No access |
|
||||
| `default_settings` | No access |
|
||||
| `organization_settings` | No access |
|
||||
| `studio_projects` | Read |
|
||||
|
||||
### Ops / Platform Admin Role
|
||||
|
||||
A role for platform operators who manage infrastructure settings but may not build agents.
|
||||
|
||||
| Feature | Permission |
|
||||
| :------------------------ | :--------- |
|
||||
| `usage_dashboards` | Manage |
|
||||
| `crews_dashboards` | Manage |
|
||||
| `invitations` | Manage |
|
||||
| `training_ui` | Read |
|
||||
| `tools` | Read |
|
||||
| `agents` | Read |
|
||||
| `environment_variables` | Manage |
|
||||
| `llm_connections` | Manage |
|
||||
| `default_settings` | Manage |
|
||||
| `organization_settings` | Read |
|
||||
| `studio_projects` | Read |
|
||||
|
||||
---
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for assistance with RBAC questions.
|
||||
</Card>
|
||||
|
||||
@@ -4,86 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 27일">
|
||||
## v1.13.0rc1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- v1.13.0a2의 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 27일">
|
||||
## v1.13.0a2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 릴리스 중 자동 업데이트 배포 테스트 리포지토리
|
||||
- 기업 릴리스의 복원력 및 사용자 경험 개선
|
||||
|
||||
### 문서
|
||||
- v1.13.0a1에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 27일">
|
||||
## v1.13.0a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- Node를 LTS 22로 고정하여 문서 작업 흐름의 끊어진 링크 수정
|
||||
- 기업 릴리스에서 새로 게시된 패키지의 uv 캐시 초기화
|
||||
|
||||
### 문서
|
||||
- 포괄적인 RBAC 권한 매트릭스 및 배포 가이드 추가
|
||||
- v1.12.2에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- devtools 릴리스에 기업 릴리스 단계 추가
|
||||
|
||||
### 버그 수정
|
||||
- @human_feedback과 함께 emit을 사용할 때 메서드 반환 값을 흐름 출력으로 유지
|
||||
|
||||
### 문서
|
||||
- v1.12.1에 대한 변경 로그 및 버전 업데이트
|
||||
- 보안 정책 및 보고 지침 수정
|
||||
|
||||
## 기여자
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 25일">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
@@ -4,86 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="27 mar 2026">
|
||||
## v1.13.0rc1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0rc1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a2
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 mar 2026">
|
||||
## v1.13.0a2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Repositório de teste de implantação de autoatualização durante o lançamento
|
||||
- Melhorar a resiliência e a experiência do usuário na versão empresarial
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.13.0a1
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 mar 2026">
|
||||
## v1.13.0a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.13.0a1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir links quebrados no fluxo de documentação fixando o Node na LTS 22
|
||||
- Limpar o cache uv para pacotes recém-publicados na versão empresarial
|
||||
|
||||
### Documentação
|
||||
- Adicionar uma matriz abrangente de permissões RBAC e guia de implantação
|
||||
- Atualizar o changelog e a versão para v1.12.2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @iris-clawd, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.12.2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar fase de lançamento empresarial ao lançamento do devtools
|
||||
|
||||
### Correções de Bugs
|
||||
- Preservar o valor de retorno do método como saída de fluxo para @human_feedback com emit
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.12.1
|
||||
- Revisar política de segurança e instruções de relatório
|
||||
|
||||
## Contributors
|
||||
|
||||
@alex-clawd, @greysonlalonde, @joaomdmoura, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 mar 2026">
|
||||
## v1.12.1
|
||||
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.13.0rc1"
|
||||
__version__ = "1.12.1"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.13.0rc1",
|
||||
"crewai==1.12.1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
@@ -140,9 +140,6 @@ contextual = [
|
||||
"contextual-client>=0.1.0",
|
||||
"nest-asyncio>=1.6.0",
|
||||
]
|
||||
sandlock = [
|
||||
"sandlock>=0.2.0",
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -309,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.13.0rc1"
|
||||
__version__ = "1.12.1"
|
||||
|
||||
@@ -1,41 +1,16 @@
|
||||
"""Code Interpreter Tool for executing Python code in isolated environments.
|
||||
|
||||
This module provides a tool for executing Python code either in a Docker container,
|
||||
a sandlock process sandbox, or directly in a restricted sandbox. It includes mechanisms
|
||||
for blocking potentially unsafe operations and importing restricted modules.
|
||||
|
||||
Execution backends (in order of preference):
|
||||
1. Docker: Full container isolation (~200ms startup)
|
||||
2. Sandlock: Kernel-level process sandbox via Landlock + seccomp-bpf (~1ms startup)
|
||||
3. Unsafe: Direct execution on the host (no isolation, trusted code only)
|
||||
|
||||
Example usage::
|
||||
|
||||
from crewai_tools import CodeInterpreterTool
|
||||
|
||||
# Auto-select best available backend (Docker > Sandlock > error)
|
||||
tool = CodeInterpreterTool()
|
||||
|
||||
# Explicitly use sandlock backend
|
||||
tool = CodeInterpreterTool(
|
||||
execution_backend="sandlock",
|
||||
sandbox_fs_read=["/usr/lib/python3", "/workspace"],
|
||||
sandbox_fs_write=["/workspace/output"],
|
||||
sandbox_max_memory_mb=512,
|
||||
)
|
||||
|
||||
# Use unsafe mode (only for trusted code)
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
This module provides a tool for executing Python code either in a Docker container for
|
||||
safe isolation or directly in a restricted sandbox. It includes mechanisms for blocking
|
||||
potentially unsafe operations and importing restricted modules.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from types import ModuleType
|
||||
from typing import Any, ClassVar, Literal, TypedDict
|
||||
from typing import Any, ClassVar, TypedDict
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
from docker import ( # type: ignore[import-untyped]
|
||||
@@ -81,7 +56,7 @@ class SandboxPython:
|
||||
sandbox escape attacks via Python object introspection. Attackers can recover the
|
||||
original __import__ function and bypass all restrictions.
|
||||
|
||||
DO NOT USE for untrusted code execution. Use Docker containers or sandlock instead.
|
||||
DO NOT USE for untrusted code execution. Use Docker containers instead.
|
||||
|
||||
This class attempts to restrict access to dangerous modules and built-in functions
|
||||
but provides no real security boundary against a motivated attacker.
|
||||
@@ -171,34 +146,8 @@ class CodeInterpreterTool(BaseTool):
|
||||
"""A tool for executing Python code in isolated environments.
|
||||
|
||||
This tool provides functionality to run Python code either in a Docker container
|
||||
for safe isolation, in a sandlock process sandbox for lightweight kernel-level
|
||||
isolation, or directly in a restricted sandbox. It can handle installing
|
||||
for safe isolation or directly in a restricted sandbox. It can handle installing
|
||||
Python packages and executing arbitrary Python code.
|
||||
|
||||
Attributes:
|
||||
execution_backend: The execution backend to use. One of ``"auto"``,
|
||||
``"docker"``, ``"sandlock"``, or ``"unsafe"``. Defaults to ``"auto"``
|
||||
which tries Docker first, then sandlock, then raises an error.
|
||||
sandbox_fs_read: List of filesystem paths to allow read access in sandlock.
|
||||
sandbox_fs_write: List of filesystem paths to allow write access in sandlock.
|
||||
sandbox_max_memory_mb: Maximum memory in MB for sandlock execution.
|
||||
sandbox_max_processes: Maximum number of processes for sandlock execution.
|
||||
sandbox_timeout: Timeout in seconds for sandlock execution.
|
||||
|
||||
Example::
|
||||
|
||||
# Auto-select best available backend
|
||||
tool = CodeInterpreterTool()
|
||||
result = tool.run(code="print('hello')", libraries_used=[])
|
||||
|
||||
# Explicitly use sandlock with custom policy
|
||||
tool = CodeInterpreterTool(
|
||||
execution_backend="sandlock",
|
||||
sandbox_fs_read=["/usr/lib/python3"],
|
||||
sandbox_fs_write=["/tmp/output"],
|
||||
sandbox_max_memory_mb=256,
|
||||
)
|
||||
result = tool.run(code="print(2 + 2)", libraries_used=[])
|
||||
"""
|
||||
|
||||
name: str = "Code Interpreter"
|
||||
@@ -210,13 +159,6 @@ class CodeInterpreterTool(BaseTool):
|
||||
user_docker_base_url: str | None = None
|
||||
unsafe_mode: bool = False
|
||||
|
||||
execution_backend: Literal["auto", "docker", "sandlock", "unsafe"] = "auto"
|
||||
sandbox_fs_read: list[str] = Field(default_factory=list)
|
||||
sandbox_fs_write: list[str] = Field(default_factory=list)
|
||||
sandbox_max_memory_mb: int | None = None
|
||||
sandbox_max_processes: int | None = None
|
||||
sandbox_timeout: int | None = None
|
||||
|
||||
@staticmethod
|
||||
def _get_installed_package_path() -> str:
|
||||
"""Gets the installation path of the crewai_tools package.
|
||||
@@ -284,17 +226,8 @@ class CodeInterpreterTool(BaseTool):
|
||||
if not code:
|
||||
return "No code provided to execute."
|
||||
|
||||
# Handle legacy unsafe_mode flag
|
||||
if self.unsafe_mode or self.execution_backend == "unsafe":
|
||||
if self.unsafe_mode:
|
||||
return self.run_code_unsafe(code, libraries_used)
|
||||
|
||||
if self.execution_backend == "docker":
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
|
||||
if self.execution_backend == "sandlock":
|
||||
return self.run_code_in_sandlock(code, libraries_used)
|
||||
|
||||
# Auto mode: try Docker first, then sandlock, then raise error
|
||||
return self.run_code_safety(code, libraries_used)
|
||||
|
||||
@staticmethod
|
||||
@@ -368,184 +301,11 @@ class CodeInterpreterTool(BaseTool):
|
||||
Printer.print("Docker is not installed", color="bold_purple")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _check_sandlock_available() -> bool:
|
||||
"""Checks if sandlock is installed and the system supports it.
|
||||
|
||||
Verifies that:
|
||||
1. The sandlock package is importable
|
||||
2. The system is running Linux (sandlock requires Linux kernel features)
|
||||
|
||||
Returns:
|
||||
True if sandlock is available and the system supports it, False otherwise.
|
||||
"""
|
||||
if platform.system() != "Linux":
|
||||
Printer.print(
|
||||
"Sandlock requires Linux (Landlock + seccomp-bpf). "
|
||||
"Use Docker on macOS/Windows.",
|
||||
color="bold_purple",
|
||||
)
|
||||
return False
|
||||
|
||||
if importlib.util.find_spec("sandlock") is None:
|
||||
Printer.print(
|
||||
"Sandlock is not installed. Install with: pip install sandlock",
|
||||
color="bold_purple",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _build_sandlock_policy(self, work_dir: str) -> Any:
|
||||
"""Builds a sandlock Policy with the configured sandbox parameters.
|
||||
|
||||
Constructs a sandlock Policy object using the tool's configuration for
|
||||
filesystem access, memory limits, process limits, and other constraints.
|
||||
|
||||
Args:
|
||||
work_dir: The working directory for the sandbox (writable).
|
||||
|
||||
Returns:
|
||||
A sandlock Policy object configured with the appropriate restrictions.
|
||||
"""
|
||||
from sandlock import Policy # type: ignore[import-untyped]
|
||||
|
||||
# Default readable paths for Python execution
|
||||
default_readable = [
|
||||
"/usr",
|
||||
"/lib",
|
||||
"/lib64",
|
||||
"/etc/alternatives",
|
||||
]
|
||||
|
||||
# Add Python-specific paths
|
||||
python_path = os.path.dirname(os.path.dirname(sys.executable))
|
||||
if python_path not in default_readable:
|
||||
default_readable.append(python_path)
|
||||
|
||||
# Include site-packages for installed libraries
|
||||
for path in sys.path:
|
||||
if path and os.path.isdir(path) and path not in default_readable:
|
||||
default_readable.append(path)
|
||||
|
||||
fs_readable = list(set(default_readable + self.sandbox_fs_read))
|
||||
fs_writable = list(set([work_dir, *self.sandbox_fs_write]))
|
||||
|
||||
policy_kwargs: dict[str, Any] = {
|
||||
"fs_readable": fs_readable,
|
||||
"fs_writable": fs_writable,
|
||||
"isolate_ipc": True,
|
||||
"clean_env": True,
|
||||
"env": {"PATH": "/usr/bin:/bin", "HOME": work_dir},
|
||||
}
|
||||
|
||||
if self.sandbox_max_memory_mb is not None:
|
||||
policy_kwargs["max_memory"] = f"{self.sandbox_max_memory_mb}M"
|
||||
|
||||
if self.sandbox_max_processes is not None:
|
||||
policy_kwargs["max_processes"] = self.sandbox_max_processes
|
||||
|
||||
return Policy(**policy_kwargs)
|
||||
|
||||
def run_code_in_sandlock(self, code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs Python code in a sandlock process sandbox.
|
||||
|
||||
Uses sandlock's Landlock + seccomp-bpf kernel-level isolation to execute
|
||||
code in a confined process. This provides stronger isolation than the
|
||||
Python-level SandboxPython (which is vulnerable to escape attacks) while
|
||||
being much lighter than Docker (~1ms vs ~200ms startup).
|
||||
|
||||
Libraries are installed in a temporary directory before sandbox activation.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
libraries_used: A list of Python library names to install before execution.
|
||||
|
||||
Returns:
|
||||
The output of the executed code as a string, or an error message
|
||||
if execution failed.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If sandlock is not available or the system doesn't support it.
|
||||
"""
|
||||
if not self._check_sandlock_available():
|
||||
raise RuntimeError(
|
||||
"Sandlock is not available. Ensure sandlock is installed "
|
||||
"(pip install sandlock) and you are running on Linux 5.13+."
|
||||
)
|
||||
|
||||
from sandlock import Sandbox
|
||||
|
||||
Printer.print(
|
||||
"Running code in sandlock sandbox (Landlock + seccomp-bpf)",
|
||||
color="bold_blue",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="crewai_sandbox_") as work_dir:
|
||||
# Install libraries before entering the sandbox
|
||||
if libraries_used:
|
||||
Printer.print(
|
||||
f"Installing libraries: {', '.join(libraries_used)}",
|
||||
color="bold_purple",
|
||||
)
|
||||
for library in libraries_used:
|
||||
subprocess.run( # noqa: S603
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--target",
|
||||
os.path.join(work_dir, "libs"),
|
||||
library,
|
||||
],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Write the code to a temporary file
|
||||
code_file = os.path.join(work_dir, "script.py")
|
||||
with open(code_file, "w") as f: # noqa: PTH123
|
||||
f.write(code)
|
||||
|
||||
# Build the sandbox policy
|
||||
policy = self._build_sandlock_policy(work_dir)
|
||||
|
||||
# Build the command with PYTHONPATH for installed libraries
|
||||
env_pythonpath = os.path.join(work_dir, "libs")
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
f"import sys; sys.path.insert(0, '{env_pythonpath}'); "
|
||||
f"exec(open('{code_file}').read())"
|
||||
),
|
||||
]
|
||||
|
||||
timeout = self.sandbox_timeout if self.sandbox_timeout is not None else 60
|
||||
|
||||
try:
|
||||
result = Sandbox(policy).run(cmd, timeout=timeout)
|
||||
output = result.stdout if hasattr(result, "stdout") else str(result)
|
||||
if hasattr(result, "returncode") and result.returncode != 0:
|
||||
stderr = result.stderr if hasattr(result, "stderr") else ""
|
||||
return (
|
||||
f"Something went wrong while running the code: "
|
||||
f"\n{stderr or output}"
|
||||
)
|
||||
return output
|
||||
except Exception as e:
|
||||
return f"An error occurred in sandlock sandbox: {e!s}"
|
||||
|
||||
def run_code_safety(self, code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs code in the safest available environment.
|
||||
|
||||
Tries execution backends in order of isolation strength:
|
||||
1. Docker (full container isolation)
|
||||
2. Sandlock (kernel-level process sandbox, Linux only)
|
||||
|
||||
Fails closed if neither backend is available.
|
||||
Requires Docker to be available for secure code execution. Fails closed
|
||||
if Docker is not available to prevent sandbox escape vulnerabilities.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
@@ -555,24 +315,18 @@ class CodeInterpreterTool(BaseTool):
|
||||
The output of the executed code as a string.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no secure execution backend is available.
|
||||
RuntimeError: If Docker is not available, as the restricted sandbox
|
||||
is vulnerable to escape attacks and should not be used
|
||||
for untrusted code execution.
|
||||
"""
|
||||
if self._check_docker_available():
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
|
||||
if self._check_sandlock_available():
|
||||
Printer.print(
|
||||
"Docker unavailable, falling back to sandlock sandbox.",
|
||||
color="bold_yellow",
|
||||
)
|
||||
return self.run_code_in_sandlock(code, libraries_used)
|
||||
|
||||
error_msg = (
|
||||
"No secure execution backend is available. "
|
||||
"Install Docker (https://docs.docker.com/get-docker/) for full container isolation, "
|
||||
"or install sandlock (pip install sandlock) on Linux 5.13+ for lightweight "
|
||||
"kernel-level sandboxing via Landlock + seccomp-bpf. "
|
||||
"Alternatively, use unsafe_mode=True or execution_backend='unsafe' "
|
||||
"Docker is required for safe code execution but is not available. "
|
||||
"The restricted sandbox fallback has been removed due to security vulnerabilities "
|
||||
"that allow sandbox escape via Python object introspection. "
|
||||
"Please install Docker (https://docs.docker.com/get-docker/) or use unsafe_mode=True "
|
||||
"if you trust the code source and understand the security risks."
|
||||
)
|
||||
Printer.print(error_msg, color="bold_red")
|
||||
@@ -618,8 +372,8 @@ class CodeInterpreterTool(BaseTool):
|
||||
- Access any Python module including os, subprocess, sys, etc.
|
||||
- Execute arbitrary commands on the host system
|
||||
|
||||
Use run_code_in_docker() or run_code_in_sandlock() for secure code execution,
|
||||
or run_code_unsafe() if you explicitly acknowledge the security risks.
|
||||
Use run_code_in_docker() for secure code execution, or run_code_unsafe()
|
||||
if you explicitly acknowledge the security risks.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from crewai_tools.tools.code_interpreter_tool.code_interpreter_tool import (
|
||||
CodeInterpreterTool,
|
||||
@@ -24,24 +23,6 @@ def docker_unavailable_mock():
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sandlock_unavailable_mock():
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.CodeInterpreterTool._check_sandlock_available",
|
||||
return_value=False,
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sandlock_available_mock():
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.CodeInterpreterTool._check_sandlock_available",
|
||||
return_value=True,
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env")
|
||||
def test_run_code_in_docker(docker_mock, printer_mock):
|
||||
tool = CodeInterpreterTool()
|
||||
@@ -96,10 +77,8 @@ print("This is line 2")"""
|
||||
)
|
||||
|
||||
|
||||
def test_docker_and_sandlock_unavailable_raises_error(
|
||||
printer_mock, docker_unavailable_mock, sandlock_unavailable_mock
|
||||
):
|
||||
"""Test that execution fails when both Docker and sandlock are unavailable."""
|
||||
def test_docker_unavailable_raises_error(printer_mock, docker_unavailable_mock):
|
||||
"""Test that execution fails when Docker is unavailable in safe mode."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = """
|
||||
result = 2 + 2
|
||||
@@ -107,9 +86,9 @@ print(result)
|
||||
"""
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert "No secure execution backend is available" in str(exc_info.value)
|
||||
assert "sandlock" in str(exc_info.value)
|
||||
|
||||
assert "Docker is required for safe code execution" in str(exc_info.value)
|
||||
assert "sandbox escape" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_blocked_modules():
|
||||
@@ -227,341 +206,6 @@ result = eval("5/1")
|
||||
assert 5.0 == result
|
||||
|
||||
|
||||
# --- Sandlock backend tests ---
|
||||
|
||||
|
||||
def test_sandlock_fallback_when_docker_unavailable(
|
||||
printer_mock, docker_unavailable_mock, sandlock_available_mock
|
||||
):
|
||||
"""Test that sandlock is used as fallback when Docker is unavailable."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "print('hello')"
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool,
|
||||
"run_code_in_sandlock",
|
||||
return_value="hello\n",
|
||||
) as sandlock_run_mock:
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert result == "hello\n"
|
||||
sandlock_run_mock.assert_called_once_with(code, [])
|
||||
|
||||
|
||||
def test_execution_backend_sandlock_calls_sandlock(
|
||||
printer_mock, sandlock_available_mock
|
||||
):
|
||||
"""Test that execution_backend='sandlock' routes to sandlock."""
|
||||
tool = CodeInterpreterTool(execution_backend="sandlock")
|
||||
code = "print('test')"
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool,
|
||||
"run_code_in_sandlock",
|
||||
return_value="test\n",
|
||||
) as mock_sandlock:
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert result == "test\n"
|
||||
mock_sandlock.assert_called_once_with(code, [])
|
||||
|
||||
|
||||
def test_execution_backend_docker_calls_docker(printer_mock):
|
||||
"""Test that execution_backend='docker' routes directly to Docker."""
|
||||
tool = CodeInterpreterTool(execution_backend="docker")
|
||||
code = "print('test')"
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool,
|
||||
"run_code_in_docker",
|
||||
return_value="test\n",
|
||||
) as mock_docker:
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert result == "test\n"
|
||||
mock_docker.assert_called_once_with(code, [])
|
||||
|
||||
|
||||
def test_execution_backend_unsafe_calls_unsafe(printer_mock):
|
||||
"""Test that execution_backend='unsafe' routes to unsafe mode."""
|
||||
tool = CodeInterpreterTool(execution_backend="unsafe")
|
||||
code = "result = 42"
|
||||
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
assert result == 42
|
||||
|
||||
|
||||
def test_sandlock_check_not_linux(printer_mock):
|
||||
"""Test that sandlock is unavailable on non-Linux systems."""
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.platform.system",
|
||||
return_value="Darwin",
|
||||
):
|
||||
assert CodeInterpreterTool._check_sandlock_available() is False
|
||||
|
||||
|
||||
def test_sandlock_check_not_installed(printer_mock):
|
||||
"""Test that sandlock is unavailable when not installed."""
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.platform.system",
|
||||
return_value="Linux",
|
||||
):
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.importlib.util.find_spec",
|
||||
return_value=None,
|
||||
):
|
||||
assert CodeInterpreterTool._check_sandlock_available() is False
|
||||
|
||||
|
||||
def test_sandlock_check_available_on_linux(printer_mock):
|
||||
"""Test that sandlock is available on Linux when installed."""
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.platform.system",
|
||||
return_value="Linux",
|
||||
):
|
||||
with patch(
|
||||
"crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.importlib.util.find_spec",
|
||||
return_value=MagicMock(), # non-None means installed
|
||||
):
|
||||
assert CodeInterpreterTool._check_sandlock_available() is True
|
||||
|
||||
|
||||
def test_sandlock_run_raises_when_unavailable(printer_mock):
|
||||
"""Test that run_code_in_sandlock raises RuntimeError when sandlock is unavailable."""
|
||||
tool = CodeInterpreterTool()
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=False
|
||||
):
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
tool.run_code_in_sandlock("print('hello')", [])
|
||||
assert "Sandlock is not available" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_sandlock_run_success(printer_mock):
|
||||
"""Test sandlock execution with successful output."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "print('hello from sandlock')"
|
||||
|
||||
sandbox_result = SimpleNamespace(
|
||||
stdout="hello from sandlock\n", stderr="", returncode=0
|
||||
)
|
||||
mock_sandbox_instance = MagicMock()
|
||||
mock_sandbox_instance.run.return_value = sandbox_result
|
||||
mock_sandbox_cls = MagicMock(return_value=mock_sandbox_instance)
|
||||
mock_policy_cls = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=True
|
||||
):
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Sandbox = mock_sandbox_cls
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
result = tool.run_code_in_sandlock(code, [])
|
||||
|
||||
assert result == "hello from sandlock\n"
|
||||
|
||||
|
||||
def test_sandlock_run_with_error(printer_mock):
|
||||
"""Test sandlock execution when the code returns an error."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "print(1/0)"
|
||||
|
||||
sandbox_result = SimpleNamespace(
|
||||
stdout="", stderr="ZeroDivisionError: division by zero", returncode=1
|
||||
)
|
||||
mock_sandbox_instance = MagicMock()
|
||||
mock_sandbox_instance.run.return_value = sandbox_result
|
||||
mock_sandbox_cls = MagicMock(return_value=mock_sandbox_instance)
|
||||
mock_policy_cls = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=True
|
||||
):
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Sandbox = mock_sandbox_cls
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
result = tool.run_code_in_sandlock(code, [])
|
||||
|
||||
assert "Something went wrong" in result
|
||||
assert "ZeroDivisionError" in result
|
||||
|
||||
|
||||
def test_sandlock_run_with_exception(printer_mock):
|
||||
"""Test sandlock execution when an exception occurs."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "print('hello')"
|
||||
|
||||
mock_sandbox_instance = MagicMock()
|
||||
mock_sandbox_instance.run.side_effect = OSError("Landlock not supported")
|
||||
mock_sandbox_cls = MagicMock(return_value=mock_sandbox_instance)
|
||||
mock_policy_cls = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=True
|
||||
):
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Sandbox = mock_sandbox_cls
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
result = tool.run_code_in_sandlock(code, [])
|
||||
|
||||
assert "An error occurred in sandlock sandbox" in result
|
||||
assert "Landlock not supported" in result
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.subprocess.run")
|
||||
def test_sandlock_installs_libraries_to_temp_dir(
|
||||
subprocess_run_mock, printer_mock
|
||||
):
|
||||
"""Test that sandlock installs libraries to a temporary directory."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "result = 1"
|
||||
libraries_used = ["numpy"]
|
||||
|
||||
sandbox_result = SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||
mock_sandbox_instance = MagicMock()
|
||||
mock_sandbox_instance.run.return_value = sandbox_result
|
||||
mock_sandbox_cls = MagicMock(return_value=mock_sandbox_instance)
|
||||
mock_policy_cls = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=True
|
||||
):
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Sandbox = mock_sandbox_cls
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
tool.run_code_in_sandlock(code, libraries_used)
|
||||
|
||||
# Check that subprocess.run was called for pip install with --target
|
||||
pip_calls = [
|
||||
c for c in subprocess_run_mock.call_args_list
|
||||
if "--target" in c[0][0]
|
||||
]
|
||||
assert len(pip_calls) == 1
|
||||
args = pip_calls[0][0][0]
|
||||
assert args[0] == sys.executable
|
||||
assert "--target" in args
|
||||
assert "numpy" in args
|
||||
|
||||
|
||||
def test_sandlock_custom_policy_params(printer_mock):
|
||||
"""Test that custom sandbox parameters are passed to the policy."""
|
||||
tool = CodeInterpreterTool(
|
||||
sandbox_fs_read=["/custom/read"],
|
||||
sandbox_fs_write=["/custom/write"],
|
||||
sandbox_max_memory_mb=256,
|
||||
sandbox_max_processes=5,
|
||||
)
|
||||
|
||||
mock_policy_cls = MagicMock()
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
tool._build_sandlock_policy("/tmp/work")
|
||||
|
||||
mock_policy_cls.assert_called_once()
|
||||
call_kwargs = mock_policy_cls.call_args[1]
|
||||
assert "/custom/read" in call_kwargs["fs_readable"]
|
||||
assert "/custom/write" in call_kwargs["fs_writable"]
|
||||
assert "/tmp/work" in call_kwargs["fs_writable"]
|
||||
assert call_kwargs["max_memory"] == "256M"
|
||||
assert call_kwargs["max_processes"] == 5
|
||||
assert call_kwargs["isolate_ipc"] is True
|
||||
assert call_kwargs["clean_env"] is True
|
||||
|
||||
|
||||
def test_sandlock_default_policy_no_memory_limit(printer_mock):
|
||||
"""Test that default policy omits max_memory when not configured."""
|
||||
tool = CodeInterpreterTool()
|
||||
|
||||
mock_policy_cls = MagicMock()
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
tool._build_sandlock_policy("/tmp/work")
|
||||
|
||||
call_kwargs = mock_policy_cls.call_args[1]
|
||||
assert "max_memory" not in call_kwargs
|
||||
assert "max_processes" not in call_kwargs
|
||||
|
||||
|
||||
def test_sandlock_timeout_default(printer_mock):
|
||||
"""Test that sandlock uses the default 60s timeout."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "print('hello')"
|
||||
|
||||
sandbox_result = SimpleNamespace(stdout="hello\n", stderr="", returncode=0)
|
||||
mock_sandbox_instance = MagicMock()
|
||||
mock_sandbox_instance.run.return_value = sandbox_result
|
||||
mock_sandbox_cls = MagicMock(return_value=mock_sandbox_instance)
|
||||
mock_policy_cls = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=True
|
||||
):
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Sandbox = mock_sandbox_cls
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
tool.run_code_in_sandlock(code, [])
|
||||
|
||||
# Verify timeout=60 was passed
|
||||
run_call = mock_sandbox_instance.run
|
||||
assert run_call.call_args[1]["timeout"] == 60
|
||||
|
||||
|
||||
def test_sandlock_custom_timeout(printer_mock):
|
||||
"""Test that sandlock uses a custom timeout when configured."""
|
||||
tool = CodeInterpreterTool(sandbox_timeout=30)
|
||||
code = "print('hello')"
|
||||
|
||||
sandbox_result = SimpleNamespace(stdout="hello\n", stderr="", returncode=0)
|
||||
mock_sandbox_instance = MagicMock()
|
||||
mock_sandbox_instance.run.return_value = sandbox_result
|
||||
mock_sandbox_cls = MagicMock(return_value=mock_sandbox_instance)
|
||||
mock_policy_cls = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_sandlock_available", return_value=True
|
||||
):
|
||||
mock_sandlock_module = MagicMock()
|
||||
mock_sandlock_module.Sandbox = mock_sandbox_cls
|
||||
mock_sandlock_module.Policy = mock_policy_cls
|
||||
with patch.dict("sys.modules", {"sandlock": mock_sandlock_module}):
|
||||
tool.run_code_in_sandlock(code, [])
|
||||
|
||||
run_call = mock_sandbox_instance.run
|
||||
assert run_call.call_args[1]["timeout"] == 30
|
||||
|
||||
|
||||
def test_auto_mode_prefers_docker_over_sandlock(printer_mock):
|
||||
"""Test that auto mode tries Docker first before sandlock."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = "print('hello')"
|
||||
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "_check_docker_available", return_value=True
|
||||
):
|
||||
with patch.object(
|
||||
CodeInterpreterTool, "run_code_in_docker", return_value="hello\n"
|
||||
) as mock_docker:
|
||||
with patch.object(
|
||||
CodeInterpreterTool,
|
||||
"run_code_in_sandlock",
|
||||
return_value="hello\n",
|
||||
) as mock_sandlock:
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
|
||||
mock_docker.assert_called_once()
|
||||
mock_sandlock.assert_not_called()
|
||||
assert result == "hello\n"
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason=(
|
||||
"run_code_in_restricted_sandbox is known to be vulnerable to sandbox "
|
||||
|
||||
@@ -5036,7 +5036,7 @@
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "A tool for executing Python code in isolated environments.\n\nThis tool provides functionality to run Python code either in a Docker container\nfor safe isolation, in a sandlock process sandbox for lightweight kernel-level\nisolation, or directly in a restricted sandbox. It can handle installing\nPython packages and executing arbitrary Python code.\n\nAttributes:\n execution_backend: The execution backend to use. One of ``\"auto\"``,\n ``\"docker\"``, ``\"sandlock\"``, or ``\"unsafe\"``. Defaults to ``\"auto\"``\n which tries Docker first, then sandlock, then raises an error.\n sandbox_fs_read: List of filesystem paths to allow read access in sandlock.\n sandbox_fs_write: List of filesystem paths to allow write access in sandlock.\n sandbox_max_memory_mb: Maximum memory in MB for sandlock execution.\n sandbox_max_processes: Maximum number of processes for sandlock execution.\n sandbox_timeout: Timeout in seconds for sandlock execution.\n\nExample::\n\n # Auto-select best available backend\n tool = CodeInterpreterTool()\n result = tool.run(code=\"print('hello')\", libraries_used=[])\n\n # Explicitly use sandlock with custom policy\n tool = CodeInterpreterTool(\n execution_backend=\"sandlock\",\n sandbox_fs_read=[\"/usr/lib/python3\"],\n sandbox_fs_write=[\"/tmp/output\"],\n sandbox_max_memory_mb=256,\n )\n result = tool.run(code=\"print(2 + 2)\", libraries_used=[])",
|
||||
"description": "A tool for executing Python code in isolated environments.\n\nThis tool provides functionality to run Python code either in a Docker container\nfor safe isolation or directly in a restricted sandbox. It can handle installing\nPython packages and executing arbitrary Python code.",
|
||||
"properties": {
|
||||
"code": {
|
||||
"anyOf": [
|
||||
@@ -5055,67 +5055,6 @@
|
||||
"title": "Default Image Tag",
|
||||
"type": "string"
|
||||
},
|
||||
"execution_backend": {
|
||||
"default": "auto",
|
||||
"enum": [
|
||||
"auto",
|
||||
"docker",
|
||||
"sandlock",
|
||||
"unsafe"
|
||||
],
|
||||
"title": "Execution Backend",
|
||||
"type": "string"
|
||||
},
|
||||
"sandbox_fs_read": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Sandbox Fs Read",
|
||||
"type": "array"
|
||||
},
|
||||
"sandbox_fs_write": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Sandbox Fs Write",
|
||||
"type": "array"
|
||||
},
|
||||
"sandbox_max_memory_mb": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Sandbox Max Memory Mb"
|
||||
},
|
||||
"sandbox_max_processes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Sandbox Max Processes"
|
||||
},
|
||||
"sandbox_timeout": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Sandbox Timeout"
|
||||
},
|
||||
"unsafe_mode": {
|
||||
"default": false,
|
||||
"title": "Unsafe Mode",
|
||||
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.13.0rc1",
|
||||
"crewai-tools==1.12.1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -42,7 +42,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.13.0rc1"
|
||||
__version__ = "1.12.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -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.13.0rc1"
|
||||
"crewai[tools]==1.12.1"
|
||||
]
|
||||
|
||||
[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.13.0rc1"
|
||||
"crewai[tools]==1.12.1"
|
||||
]
|
||||
|
||||
[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.13.0rc1"
|
||||
"crewai[tools]==1.12.1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -883,9 +883,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self.human_feedback_history: list[HumanFeedbackResult] = []
|
||||
self.last_human_feedback: HumanFeedbackResult | None = None
|
||||
self._pending_feedback_context: PendingFeedbackContext | None = None
|
||||
# Per-method stash for real @human_feedback output (keyed by method name)
|
||||
# Used to decouple routing outcome from method return value when emit is set
|
||||
self._human_feedback_method_outputs: dict[str, Any] = {}
|
||||
self._human_feedback_method_output: Any = None # Stashed real output from @human_feedback with emit
|
||||
self.suppress_flow_events: bool = suppress_flow_events
|
||||
|
||||
# User input history (for self.ask())
|
||||
@@ -2297,12 +2295,10 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
# For @human_feedback methods with emit, the result is the collapsed outcome
|
||||
# (e.g., "approved") used for routing. But we want the actual method output
|
||||
# to be the stored result (for final flow output). Replace the last entry
|
||||
# if a stashed output exists. Dict-based stash is concurrency-safe and
|
||||
# handles None return values (presence in dict = stashed, not value).
|
||||
if method_name in self._human_feedback_method_outputs:
|
||||
self._method_outputs[-1] = self._human_feedback_method_outputs.pop(
|
||||
method_name
|
||||
)
|
||||
# if a stashed output exists.
|
||||
if self._human_feedback_method_output is not None:
|
||||
self._method_outputs[-1] = self._human_feedback_method_output
|
||||
self._human_feedback_method_output = None
|
||||
|
||||
self._method_execution_counts[method_name] = (
|
||||
self._method_execution_counts.get(method_name, 0) + 1
|
||||
|
||||
@@ -594,9 +594,8 @@ def human_feedback(
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
self._human_feedback_method_output = method_output
|
||||
|
||||
return result
|
||||
|
||||
@@ -625,9 +624,8 @@ def human_feedback(
|
||||
# Stash the real method output for final flow result when emit is set
|
||||
# (result is the collapsed outcome string for routing, but we want to
|
||||
# preserve the actual method output as the flow's final result)
|
||||
# Uses per-method dict for concurrency safety and to handle None returns
|
||||
if emit:
|
||||
self._human_feedback_method_outputs[func.__name__] = method_output
|
||||
self._human_feedback_method_output = method_output
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -726,31 +726,3 @@ class TestHumanFeedbackFinalOutputPreservation:
|
||||
# _method_outputs should contain the real output
|
||||
assert len(flow._method_outputs) == 1
|
||||
assert flow._method_outputs[0] == {"data": "real output"}
|
||||
|
||||
@patch("builtins.input", return_value="looks good")
|
||||
@patch("builtins.print")
|
||||
def test_none_return_value_is_preserved(self, mock_print, mock_input):
|
||||
"""A method returning None should preserve None as flow output, not the outcome string."""
|
||||
|
||||
class NoneReturnFlow(Flow):
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
def process(self):
|
||||
# Method does work but returns None (implicit)
|
||||
pass
|
||||
|
||||
flow = NoneReturnFlow()
|
||||
|
||||
with (
|
||||
patch.object(flow, "_request_human_feedback", return_value=""),
|
||||
patch.object(flow, "_collapse_to_outcome", return_value="approved"),
|
||||
):
|
||||
result = flow.kickoff()
|
||||
|
||||
# Final output should be None (the method's real return), not "approved"
|
||||
assert result is None, f"Expected None, got {result!r}"
|
||||
assert flow.last_human_feedback.outcome == "approved"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.13.0rc1"
|
||||
__version__ = "1.12.1"
|
||||
|
||||
@@ -156,33 +156,6 @@ def update_version_in_file(file_path: Path, new_version: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def update_pyproject_version(file_path: Path, new_version: str) -> bool:
|
||||
"""Update the [project] version field in a pyproject.toml file.
|
||||
|
||||
Args:
|
||||
file_path: Path to pyproject.toml file.
|
||||
new_version: New version string.
|
||||
|
||||
Returns:
|
||||
True if version was updated, False otherwise.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
return False
|
||||
|
||||
content = file_path.read_text()
|
||||
new_content = re.sub(
|
||||
r'^(version\s*=\s*")[^"]+(")',
|
||||
rf"\g<1>{new_version}\2",
|
||||
content,
|
||||
count=1,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if new_content != content:
|
||||
file_path.write_text(new_content)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
|
||||
"crewai",
|
||||
"crewai-tools",
|
||||
@@ -1072,84 +1045,10 @@ def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
_DEPLOYMENT_TEST_REPO: Final[str] = "crewAIInc/crew_deployment_test"
|
||||
|
||||
_PYPI_POLL_INTERVAL: Final[int] = 15
|
||||
_PYPI_POLL_TIMEOUT: Final[int] = 600
|
||||
|
||||
|
||||
def _update_deployment_test_repo(version: str, is_prerelease: bool) -> None:
|
||||
"""Update the deployment test repo to pin the new crewai version.
|
||||
|
||||
Clones the repo, updates the crewai[tools] pin in pyproject.toml,
|
||||
regenerates the lockfile, commits, and pushes directly to main.
|
||||
|
||||
Args:
|
||||
version: New crewai version string.
|
||||
is_prerelease: Whether this is a pre-release version.
|
||||
"""
|
||||
console.print(
|
||||
f"\n[bold cyan]Updating {_DEPLOYMENT_TEST_REPO} to {version}[/bold cyan]"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
repo_dir = Path(tmp) / "crew_deployment_test"
|
||||
run_command(["gh", "repo", "clone", _DEPLOYMENT_TEST_REPO, str(repo_dir)])
|
||||
console.print(f"[green]✓[/green] Cloned {_DEPLOYMENT_TEST_REPO}")
|
||||
|
||||
pyproject = repo_dir / "pyproject.toml"
|
||||
content = pyproject.read_text()
|
||||
new_content = re.sub(
|
||||
r'"crewai\[tools\]==[^"]+"',
|
||||
f'"crewai[tools]=={version}"',
|
||||
content,
|
||||
)
|
||||
if new_content == content:
|
||||
console.print(
|
||||
"[yellow]Warning:[/yellow] No crewai[tools] pin found to update"
|
||||
)
|
||||
return
|
||||
pyproject.write_text(new_content)
|
||||
console.print(f"[green]✓[/green] Updated crewai[tools] pin to {version}")
|
||||
|
||||
lock_cmd = [
|
||||
"uv",
|
||||
"lock",
|
||||
"--refresh-package",
|
||||
"crewai",
|
||||
"--refresh-package",
|
||||
"crewai-tools",
|
||||
]
|
||||
if is_prerelease:
|
||||
lock_cmd.append("--prerelease=allow")
|
||||
|
||||
max_retries = 10
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
run_command(lock_cmd, cwd=repo_dir)
|
||||
break
|
||||
except subprocess.CalledProcessError:
|
||||
if attempt == max_retries:
|
||||
console.print(
|
||||
f"[red]Error:[/red] uv lock failed after {max_retries} attempts"
|
||||
)
|
||||
raise
|
||||
console.print(
|
||||
f"[yellow]uv lock failed (attempt {attempt}/{max_retries}),"
|
||||
f" retrying in {_PYPI_POLL_INTERVAL}s...[/yellow]"
|
||||
)
|
||||
time.sleep(_PYPI_POLL_INTERVAL)
|
||||
console.print("[green]✓[/green] Lockfile updated")
|
||||
|
||||
run_command(["git", "add", "pyproject.toml", "uv.lock"], cwd=repo_dir)
|
||||
run_command(
|
||||
["git", "commit", "-m", f"chore: bump crewai to {version}"],
|
||||
cwd=repo_dir,
|
||||
)
|
||||
run_command(["git", "push"], cwd=repo_dir)
|
||||
console.print(f"[green]✓[/green] Pushed to {_DEPLOYMENT_TEST_REPO}")
|
||||
|
||||
|
||||
def _wait_for_pypi(package: str, version: str) -> None:
|
||||
"""Poll PyPI until a specific package version is available.
|
||||
|
||||
@@ -1242,11 +1141,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
|
||||
pyproject = pkg_dir / "pyproject.toml"
|
||||
if pyproject.exists():
|
||||
if update_pyproject_version(pyproject, version):
|
||||
console.print(
|
||||
f"[green]✓[/green] Updated version in: "
|
||||
f"{pyproject.relative_to(repo_dir)}"
|
||||
)
|
||||
if update_pyproject_dependencies(
|
||||
pyproject, version, extra_packages=list(_ENTERPRISE_EXTRA_PACKAGES)
|
||||
):
|
||||
@@ -1265,35 +1159,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
_wait_for_pypi("crewai", version)
|
||||
|
||||
console.print("\nSyncing workspace...")
|
||||
sync_cmd = [
|
||||
"uv",
|
||||
"sync",
|
||||
"--refresh-package",
|
||||
"crewai",
|
||||
"--refresh-package",
|
||||
"crewai-tools",
|
||||
"--refresh-package",
|
||||
"crewai-files",
|
||||
]
|
||||
if is_prerelease:
|
||||
sync_cmd.append("--prerelease=allow")
|
||||
|
||||
max_retries = 10
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
run_command(sync_cmd, cwd=repo_dir)
|
||||
break
|
||||
except subprocess.CalledProcessError:
|
||||
if attempt == max_retries:
|
||||
console.print(
|
||||
f"[red]Error:[/red] uv sync failed after {max_retries} attempts"
|
||||
)
|
||||
raise
|
||||
console.print(
|
||||
f"[yellow]uv sync failed (attempt {attempt}/{max_retries}),"
|
||||
f" retrying in {_PYPI_POLL_INTERVAL}s...[/yellow]"
|
||||
)
|
||||
time.sleep(_PYPI_POLL_INTERVAL)
|
||||
run_command(["uv", "sync"], cwd=repo_dir)
|
||||
console.print("[green]✓[/green] Workspace synced")
|
||||
|
||||
# --- branch, commit, push, PR ---
|
||||
@@ -1309,7 +1175,7 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
run_command(["git", "push", "-u", "origin", branch_name], cwd=repo_dir)
|
||||
console.print("[green]✓[/green] Branch pushed")
|
||||
|
||||
pr_url = run_command(
|
||||
run_command(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
@@ -1326,7 +1192,6 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non
|
||||
cwd=repo_dir,
|
||||
)
|
||||
console.print("[green]✓[/green] Enterprise bump PR created")
|
||||
console.print(f"[cyan]PR URL:[/cyan] {pr_url}")
|
||||
|
||||
_poll_pr_until_merged(branch_name, "enterprise bump PR", repo=enterprise_repo)
|
||||
|
||||
@@ -1693,18 +1558,7 @@ def tag(dry_run: bool, no_edit: bool) -> None:
|
||||
is_flag=True,
|
||||
help="Skip the enterprise release phase",
|
||||
)
|
||||
@click.option(
|
||||
"--skip-to-enterprise",
|
||||
is_flag=True,
|
||||
help="Skip phases 1 & 2, run only the enterprise release phase",
|
||||
)
|
||||
def release(
|
||||
version: str,
|
||||
dry_run: bool,
|
||||
no_edit: bool,
|
||||
skip_enterprise: bool,
|
||||
skip_to_enterprise: bool,
|
||||
) -> None:
|
||||
def release(version: str, dry_run: bool, no_edit: bool, skip_enterprise: bool) -> None:
|
||||
"""Full release: bump versions, tag, and publish a GitHub release.
|
||||
|
||||
Combines bump and tag into a single workflow. Creates a version bump PR,
|
||||
@@ -1717,19 +1571,11 @@ def release(
|
||||
dry_run: Show what would be done without making changes.
|
||||
no_edit: Skip editing release notes.
|
||||
skip_enterprise: Skip the enterprise release phase.
|
||||
skip_to_enterprise: Skip phases 1 & 2, run only the enterprise release phase.
|
||||
"""
|
||||
try:
|
||||
check_gh_installed()
|
||||
|
||||
if skip_enterprise and skip_to_enterprise:
|
||||
console.print(
|
||||
"[red]Error:[/red] Cannot use both --skip-enterprise "
|
||||
"and --skip-to-enterprise"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not skip_enterprise or skip_to_enterprise:
|
||||
if not skip_enterprise:
|
||||
missing: list[str] = []
|
||||
if not _ENTERPRISE_REPO:
|
||||
missing.append("ENTERPRISE_REPO")
|
||||
@@ -1748,15 +1594,6 @@ def release(
|
||||
cwd = Path.cwd()
|
||||
lib_dir = cwd / "lib"
|
||||
|
||||
is_prerelease = _is_prerelease(version)
|
||||
|
||||
if skip_to_enterprise:
|
||||
_release_enterprise(version, is_prerelease, dry_run)
|
||||
console.print(
|
||||
f"\n[green]✓[/green] Enterprise release [bold]{version}[/bold] complete!"
|
||||
)
|
||||
return
|
||||
|
||||
if not dry_run:
|
||||
console.print("Checking git status...")
|
||||
check_git_clean()
|
||||
@@ -1850,8 +1687,7 @@ def release(
|
||||
|
||||
if not dry_run:
|
||||
_create_tag_and_release(tag_name, release_notes, is_prerelease)
|
||||
_trigger_pypi_publish(tag_name, wait=True)
|
||||
_update_deployment_test_repo(version, is_prerelease)
|
||||
_trigger_pypi_publish(tag_name, wait=not skip_enterprise)
|
||||
|
||||
if not skip_enterprise:
|
||||
_release_enterprise(version, is_prerelease, dry_run)
|
||||
|
||||
Reference in New Issue
Block a user