From 6e76c14384a7141bd297233d4d3043176e5c73e7 Mon Sep 17 00:00:00 2001 From: yetuge <182123929+yetuge@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:29:41 +0800 Subject: [PATCH] fix(config): reject boolean integer settings in app_config (#6171) * fix(config): reject boolean integer settings in app_config Follows the loop-detection and circuit-breaker guards: YAML booleans next to integer knobs load cleanly, and Pydantic coerces booleans to 1/0. - recursion_limit coerced from `true` to 1, so every Gateway run that does not supply its own limit hit the LangGraph recursion ceiling at the first super-step (max_recursion_limit likewise clamped the ceiling to 1). - The five llm_call integers coerced similarly: retry_max_attempts as `true` disabled retries, max_concurrent_calls as `true` serialized the pipeline. All seven integer fields now fail configuration loading with a field-specific error, while valid integers and numeric strings keep their existing behavior. Mirrors the loop-detection helper (#6017) and the in-file circuit-breaker validator. Signed-off-by: yetuge <2219677952@qq.com> * docs(changelog): record the app-config boolean rejection Changelog entries for #6171, mirroring the #6017 entry format in both languages. Signed-off-by: yetuge <2219677952@qq.com> * test(config): pin the False-side coercion regression in the boolean guards Per review: the tests only exercised `True`. On the `ge=1` fields `false` already failed the constraint pre-fix, but on the `ge=0` fields (`max_concurrent_calls`, the two delay fields) `false` coerced silently to the harmless-looking `0`, disabling the concurrency cap with no error. Parametrize both boolean tests over `[True, False]` to lock that regression in as well. Signed-off-by: yetuge <2219677952@qq.com> --------- Signed-off-by: yetuge <2219677952@qq.com> --- CHANGELOG.md | 9 ++++ CHANGELOG_zh.md | 7 +++ .../harness/deerflow/config/app_config.py | 27 +++++++++- .../test_app_config_boolean_thresholds.py | 51 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_app_config_boolean_thresholds.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3e137142..6fe48f1324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -513,6 +513,14 @@ This release closes that milestone with **301 merged pull requests**. limits to one. All integer threshold fields now fail configuration loading with a field-specific error while valid integers and numeric strings retain their existing behavior.([#6017]) +- **agents:** App-config integer settings now reject YAML booleans instead of + coercing `true` to `1`. A configuration such as `recursion_limit: true` + previously made every Gateway run that does not supply its own limit hit the + LangGraph recursion ceiling at the first super-step, and booleans on the + `llm_call` integers (`retry_max_attempts`, `max_concurrent_calls`, the two + backoff delays) collapsed retries and the concurrency cap to one. All seven + integer fields now fail configuration loading with a field-specific error + while valid integers and numeric strings retain their existing behavior.([#6171]) - **uploads:** Converted Markdown ownership is now recorded when a document is converted. `list_uploaded_files` hides only verified conversion outputs, and document outlines use only the recorded companion; a user-uploaded Markdown @@ -7611,3 +7619,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#6201]: https://github.com/bytedance/deer-flow/pull/6201 [#6202]: https://github.com/bytedance/deer-flow/pull/6202 +[#6171]: https://github.com/bytedance/deer-flow/pull/6171 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index c5171938b3..3039ee3fd5 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -454,6 +454,12 @@ 跟踪窗口、工具频率和按工具覆盖项中的布尔值也会把对应限制缩小为 1。现在所有整数阈值字段都会在配置加载阶段按字段名报错,同时保持 有效整数和数字字符串的既有行为。([#6017]) +- **智能体:** 应用配置的整数设置现在会拒绝 YAML 布尔值,而不是把 `true` + 静默转换为 `1`。此前若配置 `recursion_limit: true`,所有未自带递归上限的 + Gateway 运行都会在第一个 LangGraph 超级步就触达递归上限;`llm_call` 下的 + 整数字段(`retry_max_attempts`、`max_concurrent_calls` 与两个退避延迟)中的 + 布尔值也会把重试次数和并发上限缩小为 1。现在全部七个整数字段都会在配置 + 加载阶段按字段名报错,同时保持有效整数和数字字符串的既有行为。([#6171]) - **上传:** 文档转换时现在会记录原文件与 Markdown 的归属关系。 `list_uploaded_files` 只隐藏归属已验证的转换文件,文档大纲也只读取 记录中指定的 Markdown;用户自行上传的同名文件会正常显示,不会被 @@ -6360,5 +6366,6 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#6135]: https://github.com/bytedance/deer-flow/pull/6135 [#6138]: https://github.com/bytedance/deer-flow/pull/6138 [#6140]: https://github.com/bytedance/deer-flow/pull/6140 +[#6171]: https://github.com/bytedance/deer-flow/pull/6171 [#6201]: https://github.com/bytedance/deer-flow/pull/6201 [#6202]: https://github.com/bytedance/deer-flow/pull/6202 diff --git a/backend/packages/harness/deerflow/config/app_config.py b/backend/packages/harness/deerflow/config/app_config.py index 9ba0308625..e4fedf5f66 100644 --- a/backend/packages/harness/deerflow/config/app_config.py +++ b/backend/packages/harness/deerflow/config/app_config.py @@ -7,7 +7,7 @@ from typing import Any, Literal, Self import yaml from dotenv import load_dotenv -from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationInfo, field_validator, model_validator from deerflow.config.acp_config import ACPAgentConfig, load_acp_config_from_dict from deerflow.config.agent_storage_config import AgentStorageConfig @@ -74,6 +74,13 @@ CONFIG_FILE_DATABASE_DEFAULTS = { } +def _reject_boolean_int(value: object, info: ValidationInfo) -> object: + """Reject YAML `true`/`false` before Pydantic coerces them to `1`/`0`.""" + if isinstance(value, bool): + raise ValueError(f"{info.field_name} must be an integer, not a boolean") + return value + + class CircuitBreakerConfig(BaseModel): """Configuration for the LLM Circuit Breaker.""" @@ -152,6 +159,18 @@ class LlmCallConfig(BaseModel): ), ) + @field_validator( + "max_concurrent_calls", + "retry_max_attempts", + "retry_base_delay_ms", + "retry_cap_delay_ms", + "burst_retry_base_delay_ms", + mode="before", + ) + @classmethod + def _reject_boolean_llm_call_settings(cls, value: object, info: ValidationInfo) -> object: + return _reject_boolean_int(value, info) + class LoggingEnhanceConfig(BaseModel): """Request trace logging enhancement settings. @@ -250,6 +269,12 @@ class AppConfig(BaseModel): ge=1, description="Hard server-side ceiling for configured defaults and client-supplied run recursion_limit values. Values above this are clamped; prevents runaway LangGraph super-steps (LLM cost / DoS).", ) + + @field_validator("recursion_limit", "max_recursion_limit", mode="before") + @classmethod + def _reject_boolean_recursion_limits(cls, value: object, info: ValidationInfo) -> object: + return _reject_boolean_int(value, info) + models: list[ModelConfig] = Field(default_factory=list, description="Available models") sandbox: SandboxConfig = Field( description=format_field_description( diff --git a/backend/tests/test_app_config_boolean_thresholds.py b/backend/tests/test_app_config_boolean_thresholds.py new file mode 100644 index 0000000000..1989c5340c --- /dev/null +++ b/backend/tests/test_app_config_boolean_thresholds.py @@ -0,0 +1,51 @@ +"""YAML booleans must not coerce into the integer thresholds in app_config. + +`config.yaml` legitimately mixes booleans (`enabled: true`) with integer knobs on +neighbouring lines, so a typo like `recursion_limit: true` loads cleanly — and +Pydantic coerces it to `1`, which for the recursion limit means every Gateway run +dies at the first LangGraph super-step. Mirrors the loop-detection and circuit +breaker guards already in the repo. +""" + +import pytest +from pydantic import ValidationError + +from deerflow.config.app_config import AppConfig, LlmCallConfig +from deerflow.config.sandbox_config import SandboxConfig + +LLM_CALL_INT_FIELDS = ( + "max_concurrent_calls", + "retry_max_attempts", + "retry_base_delay_ms", + "retry_cap_delay_ms", + "burst_retry_base_delay_ms", +) + + +def _app_config(**overrides: object) -> AppConfig: + return AppConfig(sandbox=SandboxConfig(use="local"), **overrides) + + +@pytest.mark.parametrize("value", [True, False]) +@pytest.mark.parametrize("field", LLM_CALL_INT_FIELDS) +def test_llm_call_config_rejects_boolean_integers(field: str, value: bool) -> None: + with pytest.raises(ValidationError, match="must be an integer, not a boolean"): + LlmCallConfig(**{field: value}) + + +def test_llm_call_config_keeps_numeric_inputs() -> None: + config = LlmCallConfig(max_concurrent_calls="4", retry_max_attempts=3) + assert config.max_concurrent_calls == 4 + assert config.retry_max_attempts == 3 + + +@pytest.mark.parametrize("value", [True, False]) +@pytest.mark.parametrize("field", ["recursion_limit", "max_recursion_limit"]) +def test_recursion_limits_reject_booleans(field: str, value: bool) -> None: + with pytest.raises(ValidationError, match="must be an integer, not a boolean"): + _app_config(**{field: value}) + + +def test_recursion_limits_keep_numeric_inputs() -> None: + config = _app_config(recursion_limit="25") + assert config.recursion_limit == 25