mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 14:09:24 +00:00
fix: ignore extra fields in dynamic tool schemas to prevent security_context validation errors
Changes the default Pydantic config in create_model_from_schema() from extra='forbid' to extra='ignore'. This fixes OSS-9 where the framework injects security_context metadata into tool call arguments, but MCP tools and integration tools (created via create_model_from_schema) reject any extra fields with Pydantic's extra_forbidden error. Affected tools: all MCP tools (MCPServerAdapter, MCPToolResolver) and all platform integration tools (CrewAIPlatformActionTool) — these all use create_model_from_schema() without a custom __config__, so they inherited the extra='forbid' default. Regular user-defined tools (subclassing BaseModel) were not affected because BaseModel defaults to extra='ignore'. The fix is backward-compatible: - Required fields are still enforced - Type validation is still enforced - Callers can still opt into extra='forbid' via __config__ parameter - Tools that define security_context in their schema still receive it Fixes: OSS-9 Related: #4796, #4841
This commit is contained in:
@@ -623,7 +623,7 @@ def create_model_from_schema( # type: ignore[no-any-unimported]
|
||||
for name, prop in (json_schema.get("properties", {}) or {}).items()
|
||||
}
|
||||
|
||||
effective_config = __config__ or ConfigDict(extra="forbid")
|
||||
effective_config = __config__ or ConfigDict(extra="ignore")
|
||||
|
||||
return create_model_base(
|
||||
effective_name,
|
||||
|
||||
@@ -882,3 +882,129 @@ class TestEndToEndMCPSchema:
|
||||
)
|
||||
assert obj.filters.date_from == datetime.date(2025, 1, 1)
|
||||
assert obj.filters.categories == ["news", "tech"]
|
||||
|
||||
|
||||
class TestExtraFieldsIgnored:
|
||||
"""Regression tests for OSS-9: security_context injection causing
|
||||
extra_forbidden errors on MCP and integration tool schemas.
|
||||
|
||||
When the framework injects metadata like security_context into tool call
|
||||
arguments, dynamically-created Pydantic models must ignore (not reject)
|
||||
extra fields so that tool execution is not blocked.
|
||||
"""
|
||||
|
||||
SIMPLE_TOOL_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"title": "ExecuteSqlSchema",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The SQL query to execute.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
|
||||
OUTLOOK_TOOL_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"title": "MicrosoftOutlookSendEmailSchema",
|
||||
"properties": {
|
||||
"to_recipients": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of recipient email addresses.",
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "Email subject line.",
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Email body content.",
|
||||
},
|
||||
},
|
||||
"required": ["to_recipients", "subject", "body"],
|
||||
}
|
||||
|
||||
SECURITY_CONTEXT_PAYLOAD: dict[str, Any] = {
|
||||
"agent_fingerprint": {
|
||||
"user_id": "test-user-123",
|
||||
"session_id": "test-session-456",
|
||||
"metadata": {},
|
||||
},
|
||||
}
|
||||
|
||||
def test_mcp_tool_schema_ignores_security_context(self) -> None:
|
||||
"""Reproduces OSS-9 Case 1: Databricks MCP execute_sql fails when
|
||||
security_context is injected into tool args."""
|
||||
Model = create_model_from_schema(self.SIMPLE_TOOL_SCHEMA)
|
||||
# This previously raised: Extra inputs are not permitted
|
||||
# [type=extra_forbidden, input_value={'agent_fingerprint': ...}]
|
||||
obj = Model.model_validate(
|
||||
{
|
||||
"query": "SELECT * FROM my_table",
|
||||
"security_context": self.SECURITY_CONTEXT_PAYLOAD,
|
||||
}
|
||||
)
|
||||
assert obj.query == "SELECT * FROM my_table"
|
||||
# security_context should be silently dropped, not present on the model
|
||||
assert not hasattr(obj, "security_context")
|
||||
|
||||
def test_integration_tool_schema_ignores_security_context(self) -> None:
|
||||
"""Reproduces OSS-9 Case 2: Microsoft Outlook send_email fails when
|
||||
security_context is injected into tool args."""
|
||||
Model = create_model_from_schema(self.OUTLOOK_TOOL_SCHEMA)
|
||||
obj = Model.model_validate(
|
||||
{
|
||||
"to_recipients": ["user@example.com"],
|
||||
"subject": "Test",
|
||||
"body": "Hello",
|
||||
"security_context": self.SECURITY_CONTEXT_PAYLOAD,
|
||||
}
|
||||
)
|
||||
assert obj.to_recipients == ["user@example.com"]
|
||||
assert obj.subject == "Test"
|
||||
assert not hasattr(obj, "security_context")
|
||||
|
||||
def test_arbitrary_extra_fields_ignored(self) -> None:
|
||||
"""Any unexpected extra field should be silently ignored, not just
|
||||
security_context."""
|
||||
Model = create_model_from_schema(self.SIMPLE_TOOL_SCHEMA)
|
||||
obj = Model.model_validate(
|
||||
{
|
||||
"query": "SELECT 1",
|
||||
"some_unknown_field": "should be dropped",
|
||||
"another_extra": 42,
|
||||
}
|
||||
)
|
||||
assert obj.query == "SELECT 1"
|
||||
assert not hasattr(obj, "some_unknown_field")
|
||||
assert not hasattr(obj, "another_extra")
|
||||
|
||||
def test_required_fields_still_enforced(self) -> None:
|
||||
"""Changing to extra=ignore must NOT weaken required field validation."""
|
||||
Model = create_model_from_schema(self.SIMPLE_TOOL_SCHEMA)
|
||||
with pytest.raises(Exception):
|
||||
Model.model_validate({"security_context": self.SECURITY_CONTEXT_PAYLOAD})
|
||||
|
||||
def test_type_validation_still_enforced(self) -> None:
|
||||
"""Changing to extra=ignore must NOT weaken type validation."""
|
||||
Model = create_model_from_schema(self.SIMPLE_TOOL_SCHEMA)
|
||||
with pytest.raises(Exception):
|
||||
Model.model_validate({"query": 12345}) # should be string
|
||||
|
||||
def test_explicit_extra_forbid_still_works(self) -> None:
|
||||
"""Callers can still opt into extra=forbid via __config__."""
|
||||
from pydantic import ConfigDict
|
||||
|
||||
Model = create_model_from_schema(
|
||||
self.SIMPLE_TOOL_SCHEMA,
|
||||
__config__=ConfigDict(extra="forbid"),
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
Model.model_validate(
|
||||
{
|
||||
"query": "SELECT 1",
|
||||
"security_context": self.SECURITY_CONTEXT_PAYLOAD,
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user