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>
This commit is contained in:
yetuge 2026-09-28 22:36:19 +08:00 committed by GitHub
parent 5850b0da9f
commit 160fd50a28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 0 deletions

View File

@ -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(

View File

@ -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(