diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py index ded6bb40ab..f0e2b76c44 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py @@ -58,6 +58,13 @@ from crewai.state.runtime import RuntimeState +# Upper bound for agent iteration count. Prevents denial-of-wallet attacks +# where a misconfigured or malicious `max_iter` value (e.g. 10_000_000) would +# burn unbounded API credits. The default of 25 is unchanged; no legitimate +# workflow needs more than 500 iterations. +MAX_ITER_CEILING: Final[int] = 500 + + def _validate_crew_ref(value: Any) -> Any: return value @@ -284,7 +291,10 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): default_factory=list, description="Tools at agents' disposal" ) max_iter: int = Field( - default=25, description="Maximum iterations for an agent to execute a task" + default=25, + gt=0, + le=MAX_ITER_CEILING, + description="Maximum iterations for an agent to execute a task", ) agent_executor: Annotated[ SerializeAsAny[BaseAgentExecutor] | None, diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py index d251b1d36e..5397a9fdb3 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor.py @@ -24,7 +24,7 @@ class BaseAgentExecutor(BaseModel): agent: BaseAgent | None = Field(default=None, exclude=True) task: Task | None = Field(default=None, exclude=True) iterations: int = Field(default=0) - max_iter: int = Field(default=25) + max_iter: int = Field(default=25, gt=0, le=500) messages: list[LLMMessage] = Field(default_factory=list) _resuming: bool = PrivateAttr(default=False) diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 0ecd8e63a4..f40e553628 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -184,7 +184,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): prompt: SystemPromptResult | StandardPromptResult | None = Field( default=None, exclude=True ) - max_iter: int = Field(default=25, exclude=True) + max_iter: int = Field(default=25, gt=0, le=500, exclude=True) tools: list[CrewStructuredTool] = Field(default_factory=list, exclude=True) tools_names: str = Field(default="", exclude=True) stop_words: list[str] = Field(default_factory=list, exclude=True) diff --git a/lib/crewai/src/crewai/flow/runtime/__init__.py b/lib/crewai/src/crewai/flow/runtime/__init__.py index b28eb5429d..7f4bc04fc6 100644 --- a/lib/crewai/src/crewai/flow/runtime/__init__.py +++ b/lib/crewai/src/crewai/flow/runtime/__init__.py @@ -28,6 +28,7 @@ Annotated, Any, ClassVar, + Final, Generic, Literal, ParamSpec, @@ -166,6 +167,13 @@ logger = logging.getLogger(__name__) +# Upper bound on router hops within a single _route() invocation. Guards +# against circular @router definitions that would otherwise spin the event +# loop indefinitely. No legitimate flow exceeds this; if hit, surface a +# RuntimeError so the cycle is diagnosable instead of silently hanging. +MAX_ROUTER_HOPS: Final[int] = 100 + + def _condition_branches( condition: dict[str, Any], ) -> tuple[Literal["and", "or"], list[FlowDefinitionCondition]]: @@ -2734,8 +2742,15 @@ async def _execute_listeners( current_trigger = trigger_method current_result = result # Track the result to pass to each router current_triggering_event_id = triggering_event_id + hop_count = 0 while True: + hop_count += 1 + if hop_count > MAX_ROUTER_HOPS: + raise RuntimeError( + f"Flow router cycle detected: exceeded {MAX_ROUTER_HOPS} " + "router hops. Check for circular @router definitions." + ) routers_triggered = self._find_triggered_methods( current_trigger, router_only=True )