mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-10 13:02:37 +00:00
Compare commits
3 Commits
1.13.0a7
...
fix/oss-9-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c091aa63c6 | ||
|
|
9e51229e6c | ||
|
|
edd79e50ef |
@@ -10,6 +10,7 @@ from crewai.agent.core import Agent
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
from crewai.crew import Crew
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.execution_context import ExecutionContext
|
||||
from crewai.flow.flow import Flow
|
||||
from crewai.knowledge.knowledge import Knowledge
|
||||
from crewai.llm import LLM
|
||||
@@ -178,6 +179,7 @@ __all__ = [
|
||||
"BaseLLM",
|
||||
"Crew",
|
||||
"CrewOutput",
|
||||
"ExecutionContext",
|
||||
"Flow",
|
||||
"Knowledge",
|
||||
"LLMGuardrail",
|
||||
|
||||
@@ -25,13 +25,25 @@ def _get_or_create_counter() -> Iterator[int]:
|
||||
return counter
|
||||
|
||||
|
||||
_last_emitted: contextvars.ContextVar[int] = contextvars.ContextVar(
|
||||
"_last_emitted", default=0
|
||||
)
|
||||
|
||||
|
||||
def get_next_emission_sequence() -> int:
|
||||
"""Get the next emission sequence number.
|
||||
|
||||
Returns:
|
||||
The next sequence number.
|
||||
"""
|
||||
return next(_get_or_create_counter())
|
||||
seq = next(_get_or_create_counter())
|
||||
_last_emitted.set(seq)
|
||||
return seq
|
||||
|
||||
|
||||
def get_emission_sequence() -> int:
|
||||
"""Get the current emission sequence value without incrementing."""
|
||||
return _last_emitted.get()
|
||||
|
||||
|
||||
def reset_emission_counter() -> None:
|
||||
@@ -41,6 +53,14 @@ def reset_emission_counter() -> None:
|
||||
"""
|
||||
counter: Iterator[int] = itertools.count(start=1)
|
||||
_emission_counter.set(counter)
|
||||
_last_emitted.set(0)
|
||||
|
||||
|
||||
def set_emission_counter(start: int) -> None:
|
||||
"""Set the emission counter to resume from a given value."""
|
||||
counter: Iterator[int] = itertools.count(start=start + 1)
|
||||
_emission_counter.set(counter)
|
||||
_last_emitted.set(start)
|
||||
|
||||
|
||||
class BaseEvent(BaseModel):
|
||||
|
||||
80
lib/crewai/src/crewai/execution_context.py
Normal file
80
lib/crewai/src/crewai/execution_context.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Checkpointable execution context for the crewAI runtime.
|
||||
|
||||
Captures the ContextVar state needed to resume execution from a checkpoint.
|
||||
Used by the RootModel (step 5) to include execution context in snapshots.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.context import (
|
||||
_current_task_id,
|
||||
_platform_integration_token,
|
||||
)
|
||||
from crewai.events.base_events import (
|
||||
get_emission_sequence,
|
||||
set_emission_counter,
|
||||
)
|
||||
from crewai.events.event_context import (
|
||||
_event_id_stack,
|
||||
_last_event_id,
|
||||
_triggering_event_id,
|
||||
)
|
||||
from crewai.flow.flow_context import (
|
||||
current_flow_id,
|
||||
current_flow_method_name,
|
||||
current_flow_request_id,
|
||||
)
|
||||
|
||||
|
||||
class ExecutionContext(BaseModel):
|
||||
"""Snapshot of ContextVar state required for checkpoint/resume."""
|
||||
|
||||
current_task_id: str | None = Field(default=None)
|
||||
flow_request_id: str | None = Field(default=None)
|
||||
flow_id: str | None = Field(default=None)
|
||||
flow_method_name: str = Field(default="unknown")
|
||||
|
||||
event_id_stack: tuple[tuple[str, str], ...] = Field(default=())
|
||||
last_event_id: str | None = Field(default=None)
|
||||
triggering_event_id: str | None = Field(default=None)
|
||||
emission_sequence: int = Field(default=0)
|
||||
|
||||
feedback_callback_info: dict[str, Any] | None = Field(default=None)
|
||||
platform_token: str | None = Field(default=None)
|
||||
|
||||
|
||||
def capture_execution_context(
|
||||
feedback_callback_info: dict[str, Any] | None = None,
|
||||
) -> ExecutionContext:
|
||||
"""Read all checkpoint-required ContextVars into an ExecutionContext."""
|
||||
return ExecutionContext(
|
||||
current_task_id=_current_task_id.get(),
|
||||
flow_request_id=current_flow_request_id.get(),
|
||||
flow_id=current_flow_id.get(),
|
||||
flow_method_name=current_flow_method_name.get(),
|
||||
event_id_stack=_event_id_stack.get(),
|
||||
last_event_id=_last_event_id.get(),
|
||||
triggering_event_id=_triggering_event_id.get(),
|
||||
emission_sequence=get_emission_sequence(),
|
||||
feedback_callback_info=feedback_callback_info,
|
||||
platform_token=_platform_integration_token.get(),
|
||||
)
|
||||
|
||||
|
||||
def apply_execution_context(ctx: ExecutionContext) -> None:
|
||||
"""Write an ExecutionContext back into the ContextVars."""
|
||||
_current_task_id.set(ctx.current_task_id)
|
||||
current_flow_request_id.set(ctx.flow_request_id)
|
||||
current_flow_id.set(ctx.flow_id)
|
||||
current_flow_method_name.set(ctx.flow_method_name)
|
||||
|
||||
_event_id_stack.set(ctx.event_id_stack)
|
||||
_last_event_id.set(ctx.last_event_id)
|
||||
_triggering_event_id.set(ctx.triggering_event_id)
|
||||
set_emission_counter(ctx.emission_sequence)
|
||||
|
||||
_platform_integration_token.set(ctx.platform_token)
|
||||
@@ -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