Files
crewAI/pyproject.toml
João Moura 1e8cbef1b8 fix(tools): read octet-stream and xlsx urls in urlreadtool (#7261)
* fix(tools): read octet-stream URLs by sniffing the body

URLReadTool resolved content type from the Content-Type header and then
the URL path extension. Presigned object-store links carry neither: they
pin every object to application/octet-stream and use a content hash for a
path, so a SharePoint download landing in R2 was refused outright.

Sniff the already-fetched body as a third source, consulted only after the
header and both URL extensions come back with nothing. The sniff can turn
a refusal into a read but never a read into a different read, so no URL
that works today changes behavior.

Fails closed: a zip is DOCX only when word/document.xml is in its central
directory, so an .xlsx keeps its honest refusal instead of surfacing a
misleading "failed to read DOCX"; text requires a strict, whole-body UTF-8
decode with no NUL byte; an empty body identifies nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(tools): extract text from XLSX URLs

The reported presigned SharePoint link is a spreadsheet, so sniffing the
body identified it as OOXML but still had nowhere to send it: URLReadTool
had no XLSX extractor, and the file would have been refused even with a
correct spreadsheetml Content-Type.

Read workbooks with openpyxl, already a core crewai dependency, so this
adds no new one. Sheets are emitted as CSV under a "Sheet <name>:" heading,
mirroring the PDF extractor's per-page shape. read_only streams the sheets
instead of building the whole object graph and data_only takes cached
values, both of which matter for a workbook arriving from an untrusted URL.

Cells are written through csv rather than joined, so a comma, quote or
newline inside a cell cannot corrupt the grid, and trailing phantom rows
are trimmed because Excel reports sheet dimensions generously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): bound xlsx expansion and refuse ambiguous ooxml packages

Bot review found two real defects in the XLSX extractor, both reproduced.

openpyxl pads every row up to a sheet's declared dimension, so a single
stray cell far down the sheet turned a 4.8 KB upload into 100,000 rows and
200,000 cells. Trimming only trailing blanks did not help, because the
stray cell sits at the end and keeps the last row non-empty. Blank rows are
now skipped as they stream, and a cell budget caps what any one workbook
can hand an agent -- announced in the output rather than silently applied.

A zip carrying both word/document.xml and xl/workbook.xml was classified as
DOCX. Two identities is not a positive identification, so it is refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): keep whitespace-only xlsx cell values

Bot review, verified: openpyxl's row padding arrives as None, so testing
cells for exactly-empty drops it just as well as .strip() did while leaving
a row whose cells the author really did fill with spaces. And rstrip() on
the rendered grid removed a trailing space from the final cell along with
the line terminator; only the terminator should go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): bound xlsx scan work, not just emitted cells

The cell budget only counted cells that reached the output, and blank rows
skip before that point. A sheet can declare Excel's maximum dimension while
holding two real cells; openpyxl then pads every row out to 16,384 columns
and yields one row per gap. Measured: a 4,848-byte workbook drove 1.64
billion cell normalizations in 15.2 seconds with the budget never touched.

Charge a separate scan budget per row, before the row is normalized and
before the blank check, so the work a hostile sheet can demand is bounded
whether or not any of it is emitted. The regression test asserts the read
completes in under 5 seconds and is mutation-verified: dropping the per-row
charge takes it back to 26 seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(deps): clear the six pip-audit advisories

gitpython 3.1.58 has PYSEC-2026-3785 through -3788, fixed in 3.1.59; the
lock now takes 3.1.61. Its exclude-newer-package cutoff is dropped rather
than bumped -- the global 3-day cutoff has long since passed 2026-08-05, so
that per-package pin was only holding the fix back.

snowflake-sqlalchemy 1.10.0 has GHSA-8g6f-qw9x-4q6q (SQL injection and
local file disclosure), fixed in 1.11.0.

unstructured 0.18.32 has GHSA-4mvj-m6j5-pmf7, a full-read SSRF via the url=
argument of partition(). The patched 0.24.0 requires Python >=3.11 while
crewai-tools supports 3.10, so the floor carries a marker and 3.10 stays on
the old line. 0.24+ also requires beautifulsoup4>=4.14.3, so the bs4 pin
widens from ~=4.13.4 to >=4.13.4,<5 -- a widening, so no existing install
breaks. uv resolves bs4 4.13.5 on 3.10 and 4.15.0 on 3.11+.

pip-audit locally: "No known vulnerabilities found, 5 ignored", with no new
--ignore-vuln entries. Only crewai-tools[xml] grows, gaining spacy and
openai-whisper transitively through unstructured's extras.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tools): narrow bs4 find_all results without a cast

Widening the beautifulsoup4 pin let uv resolve 4.15.0 on Python 3.11+ while
3.10 stays on 4.13.5, because the old unstructured line holds it back there.
4.15 types find_all precisely, so cast(Tag, link) became redundant and mypy
failed the 3.11-3.13 type-checker jobs while 3.10 passed.

isinstance narrowing is correct under both versions and is what AGENTS.md
asks for anyway. Verified by running mypy against 4.15.0 and again against
4.13.5: browser_toolkit is clean under both, leaving only the pre-existing
errors in crewai/rag/embeddings/providers/ibm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(deps): declare security floors in crewai-tools, not only as overrides

Bot review caught a regression I introduced. override-dependencies replace
the whole requirement including its marker, so gating the unstructured
override on python_version >= '3.11' dropped the dependency outright on
3.10: the lock held only 0.24.1, never the 0.18 line the comment claimed.
crewai-tools[xml] would have installed no unstructured at all there.

Move the floors into lib/crewai-tools/pyproject.toml, where a marker split
means what it says -- >=0.24.0 on 3.11+, >=0.17.2 below -- and drop the
root override for unstructured entirely. The lock now carries both 0.18.32
and 0.24.1 under complementary markers.

Same reasoning applies to the other two, per the nltk precedent already in
that file: a uv override only shapes this workspace's lock, so consumers
installing crewai-tools[snowflake] or [github] were still getting the
vulnerable floors. Declared there now as well.

Also documents the tool as a fit for presigned and share links from S3, R2,
Google Drive, OneDrive and SharePoint -- the case this PR fixes -- while
saying plainly that it reads a URL and does not authenticate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: update tool specifications

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-04 16:51:47 +05:30

309 lines
14 KiB
TOML

name = "crewai-workspace"
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
readme = "README.md"
requires-python = ">=3.10,<3.14"
authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]
[dependency-groups]
dev = [
"ruff==0.15.1",
"mypy==1.19.1",
"pre-commit==4.5.1",
"bandit==1.9.2",
"pytest==9.0.3",
"pytest-asyncio==1.3.0",
"pytest-subprocess==1.5.3",
"vcrpy==8.2.1", # pinned, lower versions break pytest-recording
"pytest-recording==0.13.4",
"pytest-randomly==4.0.1",
"pytest-timeout==2.4.0",
"pytest-xdist==3.8.0",
"pytest-split==0.11.0",
"types-requests~=2.31.0.6",
"types-pyyaml==6.0.*",
"types-regex==2026.1.15.*",
"types-appdirs==1.4.*",
"boto3-stubs[bedrock-runtime]==1.42.40",
"types-psycopg2==2.9.21.20251012",
"types-pymysql==1.1.0.20250916",
"types-aiofiles~=25.1.0",
"types-redis~=4.6",
"commitizen>=4.13.9",
"pip-audit==2.9.0",
]
[tool.ruff]
src = ["lib/*"]
extend-exclude = [
"lib/crewai/src/crewai/cli/templates",
"lib/cli/src/crewai_cli/templates",
"lib/crewai/tests/",
"lib/crewai-tools/tests/",
"lib/cli/tests/",
]
respect-gitignore = true
force-exclude = true
fix = true
target-version = "py310"
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
future-annotations = true
extend-select = [
"E", # pycodestyle errors (style issues)
"F", # Pyflakes (code errors)
"B", # flake8-bugbear (bug prevention)
"S", # bandit (security issues)
"RUF", # ruff-specific rules
"N", # pep8-naming (naming conventions)
"W", # pycodestyle warnings
"I", # isort (import formatting)
"T", # flake8-print (print statements)
# "D", # pydocstyle (docstring conventions) disabled until
"PERF", # performance issues
"PIE", # flake8-pie (unnecessary code)
"TID", # flake8-tidy-imports (import best practices)
"ASYNC", # async/await best practices
"RET", # flake8-return (return improvements)
"SIM118", # use `key in dict` instead of `key in dict.keys()`
"UP006", # use collections.abc
"UP007", # use X | Y for unions
"UP035", # use dict/list instead of typing.Dict/List
"UP037", # remove quotes from type annotations
"UP045", # use X | None instead of Optional[X]
"UP004", # use isinstance instead of type
"UP008", # use super() instead of super(Class, self)
"UP010", # use isinstance for type checks
"UP018", # use str() instead of "string"
"UP031", # use f-strings for .format()
"UP032", # use f-strings for .format() with positional
"I001", # sort imports
"I002", # remove unused imports
]
ignore = ["E501"] # ignore line too long globally
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
[tool.ruff.lint.isort]
no-sections = false
case-sensitive = true
combine-as-imports = true
force-single-line = false
force-sort-within-sections = true
known-first-party = []
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
lines-after-imports = 2
split-on-trailing-comma = true
[tool.ruff.lint.pydocstyle]
convention = "google"
ignore-decorators = ["typing.overload"]
[tool.ruff.lint.per-file-ignores]
"lib/crewai/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements, unnecessary assignments, and hardcoded passwords in tests
"lib/crewai-tools/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "RUF012", "N818", "E402", "RUF043", "S110", "B017"] # Allow various test-specific patterns
"lib/crewai-files/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "B017", "F841"] # Allow assert statements and blind exception assertions in tests
"lib/cli/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements in tests
"lib/crewai-core/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements in tests
"lib/devtools/tests/**/*.py" = ["S101"]
[tool.mypy]
strict = true
disallow_untyped_defs = true
disallow_any_unimported = true
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true
show_error_codes = true
warn_unused_ignores = true
python_version = "3.12"
exclude = "(?x)(^lib/crewai/src/crewai/cli/templates/|^lib/cli/src/crewai_cli/templates/|^lib/crewai/tests/|^lib/crewai-tools/tests/|^lib/crewai-files/tests/|^lib/cli/tests/|^lib/devtools/tests/)"
plugins = ["pydantic.mypy"]
[tool.bandit]
exclude_dirs = ["lib/crewai/src/crewai/cli/templates", "lib/cli/src/crewai_cli/templates"]
[tool.pytest.ini_options]
markers = [
"telemetry: mark test as a telemetry test (don't mock telemetry)",
]
testpaths = [
"lib/crewai/tests",
"lib/crewai-tools/tests",
"lib/crewai-files/tests",
"lib/cli/tests",
"lib/crewai-core/tests",
]
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "function"
addopts = "--tb=short -n auto --timeout=60 --dist=loadfile --max-worker-restart=2 --block-network --import-mode=importlib"
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
[tool.commitizen]
name = "cz_customize"
version_provider = "scm"
tag_format = "$version"
allowed_prefixes = ["Merge", "Revert"]
changelog_incremental = true
update_changelog_on_bump = false
[tool.commitizen.customize]
schema = "<type>(<scope>): <description>"
schema_pattern = "^(feat|fix|refactor|perf|test|docs|chore|ci|style|revert)(\\(.+\\))?!?: .{1,72}"
bump_pattern = "^(feat|fix|perf|refactor|revert)"
bump_map = { feat = "MINOR", fix = "PATCH", perf = "PATCH", refactor = "PATCH", revert = "PATCH" }
info = "Commits must follow Conventional Commits 1.0.0."
[tool.uv]
exclude-newer = "3 days"
# These security fixes are newer than the global supply-chain cutoff.
exclude-newer-package = { msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" }
# composio-core pins rich<14 but textual requires rich>=14.
# onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10.
# fastembed 0.7.x and docling 2.63 cap pillow<12; the removed APIs don't affect them.
# langchain-core <1.2.31 has GHSA-926x-3r5x-gfhw and is required by langchain-text-splitters 1.1.2+.
# langchain-core 1.0.0-1.3.2 has GHSA-pjwx-r37v-7724 (unsafe deserialization via broad load() allowlists); force 1.3.3+.
# langchain-text-splitters <1.1.2 has GHSA-fv5p-p927-qmxr (SSRF bypass in split_text_from_url).
# transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1).
# cryptography 46.0.6 has CVE-2026-39892; force 46.0.7+.
# cryptography <=48.0.x has GHSA-m2h6-j472-rp4c, GHSA-jwv3-5hgf-82ww; fixed in 49.0.0.
# cryptography <50.0.0 has GHSA-g6cj-pr64-35w5 (PKCS#7 Bleichenbacher oracle); force 50.0.0+.
# pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p.
# pypdf <6.14.2 has GHSA-jm82-fx9c-mx94 and GHSA-5qjq-93h5-hrgp/GHSA-55h5-xmcq-c37v/GHSA-g867-7843-wf8q/GHSA-5xf7-4p34-54qr; force 6.14.2+.
# pypdf <6.15.0 has GHSA-fwg2-594c-jp42 and GHSA-fp3f-mc75-235c (unbounded runtime/memory on large content
# and /ToUnicode streams); force 6.15.0+.
# pypdf <6.16.0 has GHSA-jp53-mhqp-8xcg (infinite loop in TreeObject.insert_child).
# pypdf <6.16.1 has GHSA-23w6-3w8w-8484 and GHSA-763m-79hh-57f2 (unbounded runtime/memory on
# outlines and XForm extraction); force 6.16.1+. 6.16.2 is older than the global 3-day cutoff,
# so no exclude-newer-package override is needed.
# uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+.
# python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers).
# gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath).
# gitpython <3.1.51 has GHSA-2f96-g7mh-g2hx, GHSA-v396-v7q4-x2qj, and GHSA-956x-8gvw-wg5v.
# gitpython <=3.1.51 has GHSA-rwj8-pgh3-r573; fixed in 3.1.52.
# gitpython 3.1.52 has GHSA-3rp5-jjmw-4wv2, GHSA-fjr4-x663-mwxc, GHSA-6p8h-3wgx-97gf, and GHSA-r9mr-m37c-5fr3; force 3.1.55+.
# gitpython <3.1.56 has GHSA-p538-c434-8v24 (arbitrary file truncation via `git rev-list --output` argument
# injection) and <3.1.57 has GHSA-3f7w-8rr8-f37f (unguarded git option forwarding in IndexFile.checkout and
# TagReference); force 3.1.57+.
# gitpython <3.1.58 has GHSA-9rj7-rf2p-w77r, GHSA-4gmw-gg2m-w46p, GHSA-hh9p-6wh2-4mfc, GHSA-wvpp-8hx9-p66j and
# GHSA-jm78-9fvv-mhgr (further unguarded git option forwarding in Repo.init, read-tree and git-config, plus
# arbitrary file read via --pathspec-from-file); force 3.1.58+.
# gitpython 3.1.58 has PYSEC-2026-3785, PYSEC-2026-3786, PYSEC-2026-3787 and PYSEC-2026-3788; all fixed in
# 3.1.59, so force 3.1.59+. Its exclude-newer-package cutoff (2026-08-05) is dropped rather than bumped: the
# global 3-day cutoff is now far later than that date, so the per-package pin only blocked the fix.
# pyasn1 <0.6.4 has GHSA-8ppf-4f7h-5ppj and GHSA-hm4w-wwcw-mr6r; force 0.6.4+.
# urllib3 <2.7.0 has GHSA-qccp-gfcp-xxvc (ProxyManager cross-origin redirect leaks Authorization/Cookie) and GHSA-mf9v-mfxr-j63j (streaming decompression-bomb bypass); force 2.7.0+.
# langsmith <0.8.18 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure)
# and GHSA-f4xh-w4cj-qxq8; force 0.8.18+.
# authlib <1.6.12 has GHSA-jj8c-mmj3-mmgv (CSRF bypass in cache-based state storage) and PYSEC-2026-188.
# pip 26.1.1 has PYSEC-2026-196; force 26.1.2+.
# aiohttp <=3.13.x has GHSA-jg22-mg44-37j8, GHSA-hg6j-4rv6-33pg; fixed in 3.14.0; force 3.14.0+.
# aiohttp <=3.14.1 has GHSA-mq44-7p77-q5h7, GHSA-mfx4-hv73-q22v; fixed in 3.14.2.
# aiohttp <=3.14.2 has GHSA-cq5v-8q36-5273 (C parser OOB read); force 3.14.3+.
# docling-core 2.74.0 has GHSA-j5xp-7m2f-49jv, GHSA-jmmv-h3mp-59v8; force 2.74.1+.
# pip <26.1.1 has GHSA-58qw-9mgm-455v (archive handling); OSV considers 26.1.1 unaffected.
# paramiko <5.0.0 has GHSA-r374-rxx8-8654 (SHA-1 in rsakey.py); OSV considers 5.0.0 unaffected. Transitive via composio-core.
# starlette <1.3.1 has PYSEC-2026-161, GHSA-jp82-jpqv-5vv3, and GHSA-82w8-qh3p-5jfq. Transitive via fastapi.
# msgpack <1.2.1 has GHSA-6v7p-g79w-8964; transitive via pip-audit[filecache].
# nltk <3.10.0 has GHSA-qvv7-cg9c-w4x3 (DNS-rebinding SSRF bypass in
# nltk.pathsec.urlopen), GHSA-fg7f-2386-8897 (ReDoS in ReviewsCorpusReader), and
# GHSA-xh95-f55m-82fw (path traversal in FramenetCorpusReader.frame); all fixed
# in 3.10.0. 3.10.0 also clears PYSEC-2026-597, whose last affected version is
# 3.9.4, so that ignore is no longer needed. 3.10.0-3.10.1 have PYSEC-2026-3726
# (symlink-based arbitrary file read in IPIPANCorpusReader); fixed in 3.10.2.
# 3.10.3 also clears later 3.10.2 findings (proxy SSRF, pickle allowlist RCE,
# JVM option injection, XML entity expansion). 3.10.3 still has
# GHSA-8mgp-746c-j5xp (CVE-2026-81726; model-artifact pathsec bypass); no
# patched PyPI release yet, so that GHSA is ignored in pip-audit until one
# ships. TODO: drop --ignore-vuln GHSA-8mgp-746c-j5xp when bumping nltk
# past 3.10.3. Transitive via
# crewai-tools[xml] -> unstructured.
# pydantic-settings <2.14.2 has GHSA-4xgf-cpjx-pc3j.
# h2 <=4.4.0 has GHSA-6hr6-w5qg-qmwg (CVE-2026-71554): duplicate Host headers
# can facilitate request smuggling; fixed in 4.4.1. Transitive via
# qdrant-client -> httpx[http2].
# torch <=2.12.1 has GHSA-rrmf-rvhw-rf47 (CVE-2025-3000): memory corruption in
# torch.jit.script; fixed in 2.13.0. Transitive via docling/unstructured extras.
# snowflake-sqlalchemy <1.11.0 has GHSA-8g6f-qw9x-4q6q (SQL injection and local file disclosure); fixed in
# 1.11.0. Declared as crewai-tools[snowflake] "snowflake-sqlalchemy>=1.7.3", which the lock resolved to 1.10.0.
# unstructured <0.24.0 has GHSA-4mvj-m6j5-pmf7 (full-read SSRF via partition(url=)); the marker-split floor
# lives in lib/crewai-tools/pyproject.toml rather than here, because an override replaces the whole
# requirement including its marker and would drop the dependency on 3.10. 0.24+ needs beautifulsoup4>=4.14.3,
# which is why the crewai-tools bs4 pin widens from ~=4.13.4 to >=4.13.4,<5 -- a widening, so no existing
# install breaks; uv resolves bs4 4.13.5 on 3.10 and 4.15.0 on 3.11+. Only crewai-tools[xml] grows, gaining
# spacy and openai-whisper transitively on 3.11+.
# snowflake-connector-python >=4.0.0,<4.7.1 has GHSA-5cc2-282f-jjq2 (CVE-2026-15925):
# TLS hostnames are not verified, so a network attacker can impersonate the endpoint;
# fixed in 4.7.1. Declared as crewai-tools[snowflake] "snowflake-connector-python>=3.12.4",
# which the lock resolved to 4.6.0.
# Keep OpenAI on the SDK range required by CrewAI when transitive dependencies
# loosen or pin their own lower versions.
override-dependencies = [
"openai>=2.30.0,<3",
"rich>=13.7.1",
"onnxruntime<1.24; python_version < '3.11'",
"pillow>=12.3.0",
"langchain-core>=1.3.3,<2",
"langchain-text-splitters>=1.1.2,<2",
"urllib3>=2.7.0",
"transformers>=5.4.0; python_version >= '3.10'",
"cryptography>=50.0.0",
"pypdf>=6.16.1,<7",
"uv>=0.11.15,<1",
"python-multipart>=0.0.27,<1",
"gitpython>=3.1.59,<4",
"pyasn1>=0.6.4",
"langsmith>=0.8.18,<1",
"authlib>=1.6.12",
"pip>=26.2", # PYSEC-2026-3721 / CVE-2026-13346 — fixed in 26.2
"aiohttp>=3.14.3",
# [chunking] carried here because override-dependencies replace the whole
# requirement; without it the docling extra's chunking deps get stripped.
"docling-core[chunking]>=2.74.1",
"paramiko>=5.0.0",
"starlette>=1.3.1",
"msgpack>=1.2.1",
"pydantic-settings>=2.14.2",
"setuptools>=83.0.0", # PYSEC-2026-3447
"nltk>=3.10.3",
"h2>=4.4.1",
"torch>=2.13.0",
"snowflake-connector-python>=4.7.1",
"snowflake-sqlalchemy>=1.11.0",
]
[tool.uv.workspace]
members = [
"lib/crewai",
"lib/crewai-tools",
"lib/devtools",
"lib/crewai-files",
"lib/cli",
"lib/crewai-core",
]
[tool.uv.sources]
crewai = { workspace = true }
crewai-tools = { workspace = true }
crewai-devtools = { workspace = true }
crewai-files = { workspace = true }
crewai-cli = { workspace = true }
crewai-core = { workspace = true }