From c06043f7e8a9c130117beb2a21a306c02af293da Mon Sep 17 00:00:00 2001 From: alex-clawd Date: Fri, 24 Jul 2026 09:49:05 -0700 Subject: [PATCH] fix: preserve strict tool schema property names (#6628) --- lib/crewai-files/pyproject.toml | 6 +-- .../crewai/utilities/pydantic_schema_utils.py | 16 +++++-- .../utilities/test_pydantic_schema_utils.py | 44 ++++++++++++++++++- pyproject.toml | 6 +-- uv.lock | 14 +++--- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/lib/crewai-files/pyproject.toml b/lib/crewai-files/pyproject.toml index 398d94068..c19a5159f 100644 --- a/lib/crewai-files/pyproject.toml +++ b/lib/crewai-files/pyproject.toml @@ -9,7 +9,7 @@ authors = [ requires-python = ">=3.10, <3.14" dependencies = [ "Pillow~=12.3.0", - "pypdf~=6.13.3", + "pypdf~=6.14.2", "python-magic>=0.4.27", "aiocache~=0.12.3", "aiofiles~=24.1.0", @@ -19,8 +19,8 @@ dependencies = [ [tool.uv] exclude-newer = "3 days" -# pypdf 6.13.3 is a security fix newer than the global supply-chain cutoff. -exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" } +# pypdf 6.14.2 is a security fix newer than the global supply-chain cutoff. +exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z" } [build-system] requires = ["hatchling"] diff --git a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py index 85a53d9bc..a5218a5de 100644 --- a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py +++ b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py @@ -549,7 +549,13 @@ _CLAUDE_STRICT_UNSUPPORTED: Final[tuple[str, ...]] = ( def _strip_keys_recursive( d: Any, keys: tuple[str, ...], _seen: set[int] | None = None ) -> Any: - """Recursively delete a fixed set of keys from a schema.""" + """Recursively delete schema metadata keys without deleting property names. + + JSON Schema stores fields under a ``properties`` map. A user/tool argument can + legitimately be named ``title``, ``default``, or another metadata key. Those + entries must stay in the map; only metadata inside the schema nodes should be + stripped. + """ if _seen is None: _seen = set() if isinstance(d, dict): @@ -558,8 +564,12 @@ def _strip_keys_recursive( _seen.add(id(d)) for key in keys: d.pop(key, None) - for v in d.values(): - _strip_keys_recursive(v, keys, _seen) + for key, value in d.items(): + if key == "properties" and isinstance(value, dict): + for property_schema in value.values(): + _strip_keys_recursive(property_schema, keys, _seen) + else: + _strip_keys_recursive(value, keys, _seen) elif isinstance(d, list): if id(d) in _seen: return d diff --git a/lib/crewai/tests/utilities/test_pydantic_schema_utils.py b/lib/crewai/tests/utilities/test_pydantic_schema_utils.py index 5c0c4118c..e95ec9d10 100644 --- a/lib/crewai/tests/utilities/test_pydantic_schema_utils.py +++ b/lib/crewai/tests/utilities/test_pydantic_schema_utils.py @@ -933,7 +933,49 @@ class TestResolveRefsRecursive: assert resolved["properties"]["x"]["type"] == "integer" -class TestSanitizeRecursiveSchemas: +class TestSanitizeStrictSchemas: + def test_openai_strict_preserves_property_named_title(self) -> None: + from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict + + schema = { + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "url": {"title": "Url", "type": "string"}, + }, + "required": ["title", "url"], + } + + san = sanitize_tool_params_for_openai_strict(deepcopy(schema)) + + assert "title" in san["properties"] + assert set(san["required"]) == set(san["properties"].keys()) + assert "title" not in san["properties"]["title"] + + def test_openai_strict_preserves_nested_property_named_title(self) -> None: + from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_openai_strict + + schema = { + "type": "object", + "properties": { + "payload": { + "type": "object", + "properties": { + "title": {"title": "Nested Title", "type": "string"}, + }, + "required": ["title"], + }, + }, + "required": ["payload"], + } + + san = sanitize_tool_params_for_openai_strict(deepcopy(schema)) + payload = san["properties"]["payload"] + + assert "title" in payload["properties"] + assert payload["required"] == ["title"] + assert "title" not in payload["properties"]["title"] + def test_anthropic_strict_preserves_recursive_type(self) -> None: from crewai.utilities.pydantic_schema_utils import sanitize_tool_params_for_anthropic_strict diff --git a/pyproject.toml b/pyproject.toml index cc08157c0..70352aff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,7 +172,7 @@ 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 = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z" } +exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", 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. @@ -183,7 +183,7 @@ exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z", msgpack = "2026-06-20T # 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+. # pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p. -# pypdf <6.13.3 has GHSA-jm82-fx9c-mx94; force 6.13.3+. +# 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+. # 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). @@ -213,7 +213,7 @@ override-dependencies = [ "urllib3>=2.7.0", "transformers>=5.4.0; python_version >= '3.10'", "cryptography>=46.0.7", - "pypdf>=6.13.3,<7", + "pypdf>=6.14.2,<7", "uv>=0.11.15,<1", "python-multipart>=0.0.27,<1", "gitpython>=3.1.51,<4", diff --git a/uv.lock b/uv.lock index 3552d5d79..995818bfc 100644 --- a/uv.lock +++ b/uv.lock @@ -13,13 +13,13 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-07-21T05:40:28.001333135Z" exclude-newer-span = "P3D" [options.exclude-newer-package] msgpack = "2026-06-20T00:00:00Z" langsmith = "2026-06-20T00:00:00Z" -pypdf = "2026-06-18T00:00:00Z" +pypdf = "2026-06-24T00:00:00Z" pydantic-settings = "2026-06-20T00:00:00Z" [manifest] @@ -48,7 +48,7 @@ overrides = [ { name = "pip", specifier = ">=26.1.2" }, { name = "pyasn1", specifier = ">=0.6.4" }, { name = "pydantic-settings", specifier = ">=2.14.2" }, - { name = "pypdf", specifier = ">=6.13.3,<7" }, + { name = "pypdf", specifier = ">=6.14.2,<7" }, { name = "python-multipart", specifier = ">=0.0.27,<1" }, { name = "rich", specifier = ">=13.7.1" }, { name = "setuptools", specifier = ">=83.0.0" }, @@ -1595,7 +1595,7 @@ requires-dist = [ { name = "aiofiles", specifier = "~=24.1.0" }, { name = "av", specifier = "~=13.0.0" }, { name = "pillow", specifier = "~=12.3.0" }, - { name = "pypdf", specifier = "~=6.13.3" }, + { name = "pypdf", specifier = "~=6.14.2" }, { name = "python-magic", specifier = ">=0.4.27" }, { name = "tinytag", specifier = "~=2.2.1" }, ] @@ -7181,14 +7181,14 @@ wheels = [ [[package]] name = "pypdf" -version = "6.13.3" +version = "6.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063, upload-time = "2026-06-17T15:22:00.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288, upload-time = "2026-06-17T15:21:59.512Z" }, + { url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" }, ] [[package]]