From 160fd50a28c8324d54a12f422ac06221cc8b1cb0 Mon Sep 17 00:00:00 2001 From: yetuge <182123929+yetuge@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:36:19 +0800 Subject: [PATCH] fix(sandbox): reject non-finite health_check_skip_seconds in config (#5997) `health_check_skip_seconds` was the only float field in SandboxConfig without `allow_inf_nan=False`, and `ge=0` alone does not stop inf (`inf >= 0` is true). A config.yaml value of `1e999` loads as the string "1e999" and pydantic's float coercion turns it into inf, so the field accepted it and the BoxLite warm-reuse site then skipped the health check permanently: `skip_seconds > 0 and (time.time() - released_at) < skip_seconds` holds forever for inf, so every box this instance recently released is promoted without validation instead of failing fast at config load. Add `allow_inf_nan=False`, matching the sibling sandbox float fields (`bash_command_timeout`, `renewal_interval_seconds`, `ttl_multiplier`) and the ownership lease-TTL model validator. Red/green: `test_sandbox_config_rejects_non_finite_health_check_skip_seconds` parametrizes inf and "1e999"; both were accepted before the fix (RED) and are rejected after. Full boxlite suite: 54 passed / 15 skipped. Regression: the sandbox/config/ownership subset fails 17 tests on this Windows host both with and without the change, with a byte-identical failure-name set (environment-type: process-spawn and file-stat behavior), so no regressions are introduced. Signed-off-by: yetuge <2219677952@qq.com> --- .../harness/deerflow/config/sandbox_config.py | 1 + backend/tests/test_boxlite_provider.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/backend/packages/harness/deerflow/config/sandbox_config.py b/backend/packages/harness/deerflow/config/sandbox_config.py index 4ce45a4ace..cc42e380a1 100644 --- a/backend/packages/harness/deerflow/config/sandbox_config.py +++ b/backend/packages/harness/deerflow/config/sandbox_config.py @@ -236,6 +236,7 @@ class SandboxConfig(BaseModel): health_check_skip_seconds: float | None = Field( default=None, ge=0, + allow_inf_nan=False, description="BoxLite-only reclaim skip window in seconds for boxes recently released by this provider instance. Set to 0 to always validate before warm reuse.", ) ownership: SandboxOwnershipConfig | None = Field( diff --git a/backend/tests/test_boxlite_provider.py b/backend/tests/test_boxlite_provider.py index 736d6d45e6..138fbf56ef 100644 --- a/backend/tests/test_boxlite_provider.py +++ b/backend/tests/test_boxlite_provider.py @@ -18,9 +18,11 @@ import time import types import pytest +from pydantic import ValidationError from deerflow.community.boxlite.box import BoxliteBox from deerflow.community.boxlite.provider import BoxliteProvider, _import_simplebox +from deerflow.config.sandbox_config import SandboxConfig from deerflow.trace_context import get_current_trace_id, request_trace_context _LEGACY_COLLIDING_IDENTITIES = ( @@ -543,6 +545,25 @@ async def test_acquire_async_propagates_request_trace_context(monkeypatch): await asyncio.to_thread(provider.shutdown) +@pytest.mark.parametrize("value", [float("inf"), "1e999"]) +def test_sandbox_config_rejects_non_finite_health_check_skip_seconds(value): + """`ge=0` alone lets a non-finite skip window through. + + `health_check_skip_seconds: 1e999` in config.yaml loads as the string + "1e999" and pydantic's float coercion turns it into inf; `ge=0` accepts + inf (inf >= 0). At the warm-reuse site the check `skip_seconds > 0 and + (now - released_at) < skip_seconds` then holds forever, so recently + released boxes are promoted without the health check permanently instead + of failing fast at config load. `allow_inf_nan=False` (as already set on + the sibling sandbox float fields) makes the config layer reject it. + """ + with pytest.raises(ValidationError): + SandboxConfig( + use="deerflow.community.boxlite.provider:BoxliteProvider", + health_check_skip_seconds=value, + ) + + def test_explicit_recent_reclaim_skip_avoids_health_check(monkeypatch): """A configured skip window can reclaim recently released boxes without a ping.""" monkeypatch.setattr(