mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 10:56:02 +00:00
fix(sandbox): bypass proxies for local AIO traffic (#4444)
* fix(sandbox): bypass proxies for local AIO traffic * fix(sandbox): classify public IPv6 proxy targets
This commit is contained in:
parent
090e80c1dd
commit
2e5c8da257
@ -249,6 +249,11 @@ make docker-start # Start services (auto-detects sandbox mode from config.yaml
|
|||||||
|
|
||||||
Docker builds use the upstream `uv` registry by default. If you need faster mirrors in restricted networks, export `UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple` and `NPM_REGISTRY=https://registry.npmmirror.com` before running `make docker-init` or `make docker-start`.
|
Docker builds use the upstream `uv` registry by default. If you need faster mirrors in restricted networks, export `UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple` and `NPM_REGISTRY=https://registry.npmmirror.com` before running `make docker-init` or `make docker-start`.
|
||||||
|
|
||||||
|
Local AIO sandbox control traffic is always direct: loopback/private addresses,
|
||||||
|
single-label cluster hosts, and Docker/Podman internal hostnames do not inherit
|
||||||
|
`HTTP_PROXY` or `HTTPS_PROXY`. External sandbox FQDNs and public IPs still
|
||||||
|
honor environment proxy settings.
|
||||||
|
|
||||||
Backend processes automatically pick up `config.yaml` changes on the next config access, so model metadata updates do not require a manual restart during development.
|
Backend processes automatically pick up `config.yaml` changes on the next config access, so model metadata updates do not require a manual restart during development.
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
|
|||||||
@ -530,7 +530,7 @@ copying a raw checkpoint because delta state is not self-contained in one tuple.
|
|||||||
**Environment policy** (`sandbox/env_policy.py`): `execute_command` no longer inherits the full `os.environ`. `build_sandbox_env()` scrubs secret-looking names (`*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASS*`/`*CREDENTIAL*`) from the inherited environment before layering injected request secrets on top, so platform credentials (e.g. `OPENAI_API_KEY`) never leak into skill subprocesses. Benign vars (`PATH`, `HOME`, `LANG`, `VIRTUAL_ENV`, ...) are preserved.
|
**Environment policy** (`sandbox/env_policy.py`): `execute_command` no longer inherits the full `os.environ`. `build_sandbox_env()` scrubs secret-looking names (`*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASS*`/`*CREDENTIAL*`) from the inherited environment before layering injected request secrets on top, so platform credentials (e.g. `OPENAI_API_KEY`) never leak into skill subprocesses. Benign vars (`PATH`, `HOME`, `LANG`, `VIRTUAL_ENV`, ...) are preserved.
|
||||||
**Implementations**:
|
**Implementations**:
|
||||||
- `LocalSandboxProvider` - Local filesystem execution. `acquire(thread_id)` returns a per-thread `LocalSandbox` (id `local:{thread_id}`) whose `path_mappings` resolve `/mnt/user-data/{workspace,uploads,outputs}` and `/mnt/acp-workspace` to that thread's host directories, so the public `Sandbox` API honours the `/mnt/user-data` contract uniformly with AIO. `acquire()` / `acquire(None)` keeps the legacy generic singleton (id `local`) for callers without a thread context. Per-thread sandboxes are held in an LRU cache (default 256 entries) guarded by a `threading.Lock`. Legacy global-custom mounts are gated by the same user-scoped skill discovery rule used for prompt/list visibility; providers must not infer visibility from raw directory presence alone.
|
- `LocalSandboxProvider` - Local filesystem execution. `acquire(thread_id)` returns a per-thread `LocalSandbox` (id `local:{thread_id}`) whose `path_mappings` resolve `/mnt/user-data/{workspace,uploads,outputs}` and `/mnt/acp-workspace` to that thread's host directories, so the public `Sandbox` API honours the `/mnt/user-data` contract uniformly with AIO. `acquire()` / `acquire(None)` keeps the legacy generic singleton (id `local`) for callers without a thread context. Per-thread sandboxes are held in an LRU cache (default 256 entries) guarded by a `threading.Lock`. Legacy global-custom mounts are gated by the same user-scoped skill discovery rule used for prompt/list visibility; providers must not infer visibility from raw directory presence alone.
|
||||||
- `AioSandboxProvider` (`packages/harness/deerflow/community/`) - Docker-based isolation. Active-cache and warm-pool entries are checked with the backend during acquire/reuse; definitively dead containers are dropped from all in-process maps so the thread can discover or create a fresh sandbox instead of reusing a stale client. Backend health-check failures are treated as unknown, not dead; local discovery likewise treats an unverifiable container as not adoptable and falls through to create rather than failing acquire. `get()` remains an in-memory lookup for event-loop-safe tool paths — it never touches the ownership store (that would be blocking IO on the event loop); ownership is published on acquire/reclaim and refreshed off the event loop by the dedicated renewal thread (`_renew_owned_leases`). Legacy global-custom mounts follow the same shared visibility helper as local and remote providers.
|
- `AioSandboxProvider` (`packages/harness/deerflow/community/`) - Docker-based isolation. Active-cache and warm-pool entries are checked with the backend during acquire/reuse; definitively dead containers are dropped from all in-process maps so the thread can discover or create a fresh sandbox instead of reusing a stale client. Backend health-check failures are treated as unknown, not dead; local discovery likewise treats an unverifiable container as not adoptable and falls through to create rather than failing acquire. `get()` remains an in-memory lookup for event-loop-safe tool paths — it never touches the ownership store (that would be blocking IO on the event loop); ownership is published on acquire/reclaim and refreshed off the event loop by the dedicated renewal thread (`_renew_owned_leases`). Legacy global-custom mounts follow the same shared visibility helper as local and remote providers. Readiness probes and `agent_sandbox` clients classify loopback/private IPs, single-label cluster hosts, and Docker/Podman internal hostnames as direct control-plane destinations and set `trust_env=False`; external FQDNs and public IPs retain environment proxy support.
|
||||||
- `E2BSandboxProvider` (`packages/harness/deerflow/community/e2b_sandbox/`) provides E2B remote isolation.
|
- `E2BSandboxProvider` (`packages/harness/deerflow/community/e2b_sandbox/`) provides E2B remote isolation.
|
||||||
Acquire and release share a per-user and thread lock. The provider lock does
|
Acquire and release share a per-user and thread lock. The provider lock does
|
||||||
not cover remote IO. `burst_limit` adds capacity only for the `burst` policy.
|
not cover remote IO. `burst_limit` adds capacity only for the `burst` policy.
|
||||||
|
|||||||
@ -505,6 +505,12 @@ When you configure `sandbox.mounts`, DeerFlow exposes those `container_path` val
|
|||||||
|
|
||||||
For bare-metal Docker sandbox runs that use localhost, DeerFlow binds the sandbox HTTP port to `127.0.0.1` by default so it is not exposed on every host interface. Docker-outside-of-Docker deployments that connect through `host.docker.internal` keep the broad legacy bind for compatibility. Set `DEER_FLOW_SANDBOX_BIND_HOST` explicitly if your deployment needs a different bind address.
|
For bare-metal Docker sandbox runs that use localhost, DeerFlow binds the sandbox HTTP port to `127.0.0.1` by default so it is not exposed on every host interface. Docker-outside-of-Docker deployments that connect through `host.docker.internal` keep the broad legacy bind for compatibility. Set `DEER_FLOW_SANDBOX_BIND_HOST` explicitly if your deployment needs a different bind address.
|
||||||
|
|
||||||
|
Sandbox control-plane HTTP calls to loopback/private IPs, single-label cluster
|
||||||
|
hosts, and Docker/Podman internal hostnames bypass `HTTP_PROXY`/`HTTPS_PROXY`
|
||||||
|
inside the client. This prevents an inherited proxy from returning a misleading
|
||||||
|
502 for a healthy local sandbox. Externally hosted sandbox FQDNs and public IPs
|
||||||
|
continue to use the normal environment proxy configuration.
|
||||||
|
|
||||||
### Building a Custom AIO Sandbox Image
|
### Building a Custom AIO Sandbox Image
|
||||||
|
|
||||||
`AioSandboxProvider` talks to the sandbox container through the `agent-sandbox` SDK. The Dockerfile for the default `enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest` image is not part of this repository; DeerFlow treats that image as an upstream AIO sandbox runtime.
|
`AioSandboxProvider` talks to the sandbox container through the `agent-sandbox` SDK. The Dockerfile for the default `enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest` image is not part of this repository; DeerFlow treats that image as an upstream AIO sandbox runtime.
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import shlex
|
|||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import httpx
|
||||||
from agent_sandbox import Sandbox as AioSandboxClient
|
from agent_sandbox import Sandbox as AioSandboxClient
|
||||||
from agent_sandbox.core.api_error import ApiError
|
from agent_sandbox.core.api_error import ApiError
|
||||||
|
|
||||||
@ -12,6 +13,8 @@ from deerflow.config.paths import VIRTUAL_PATH_PREFIX
|
|||||||
from deerflow.sandbox.sandbox import Sandbox, _validate_extra_env
|
from deerflow.sandbox.sandbox import Sandbox, _validate_extra_env
|
||||||
from deerflow.sandbox.search import GrepMatch, path_matches, should_ignore_path, truncate_line
|
from deerflow.sandbox.search import GrepMatch, path_matches, should_ignore_path, truncate_line
|
||||||
|
|
||||||
|
from .backend import sandbox_http_trust_env
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_MAX_DOWNLOAD_SIZE = 100 * 1024 * 1024 # 100 MB
|
_MAX_DOWNLOAD_SIZE = 100 * 1024 * 1024 # 100 MB
|
||||||
@ -50,7 +53,15 @@ class AioSandbox(Sandbox):
|
|||||||
"""
|
"""
|
||||||
super().__init__(id)
|
super().__init__(id)
|
||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._client = AioSandboxClient(base_url=base_url, timeout=600)
|
if sandbox_http_trust_env(base_url):
|
||||||
|
self._client = AioSandboxClient(base_url=base_url, timeout=600)
|
||||||
|
else:
|
||||||
|
direct_client = httpx.Client(timeout=600, follow_redirects=True, trust_env=False)
|
||||||
|
self._client = AioSandboxClient(
|
||||||
|
base_url=base_url,
|
||||||
|
timeout=600,
|
||||||
|
httpx_client=direct_client,
|
||||||
|
)
|
||||||
self._home_dir = home_dir
|
self._home_dir = home_dir
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import requests
|
import requests
|
||||||
@ -15,6 +17,30 @@ from .sandbox_info import SandboxInfo
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sandbox_http_trust_env(sandbox_url: str) -> bool:
|
||||||
|
"""Whether HTTP clients for *sandbox_url* should inherit proxy settings.
|
||||||
|
|
||||||
|
Local Docker, DooD, and Kubernetes sandbox endpoints are control-plane
|
||||||
|
connections, not internet traffic. Sending them through ``HTTP_PROXY`` can
|
||||||
|
produce a misleading proxy-generated 502 even though the sandbox container
|
||||||
|
is healthy (#3441). External fully-qualified hosts retain normal environment
|
||||||
|
proxy behavior.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
hostname = (urlparse(sandbox_url).hostname or "").rstrip(".").lower()
|
||||||
|
except ValueError:
|
||||||
|
return True
|
||||||
|
if not hostname:
|
||||||
|
return True
|
||||||
|
if hostname == "localhost" or hostname.endswith(".localhost") or hostname.endswith(".docker.internal") or hostname.endswith(".containers.internal"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
address = ipaddress.ip_address(hostname)
|
||||||
|
except ValueError:
|
||||||
|
return "." in hostname
|
||||||
|
return not (address.is_loopback or address.is_private or address.is_link_local)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_sandbox_ready(sandbox_url: str, timeout: int = 30) -> bool:
|
def wait_for_sandbox_ready(sandbox_url: str, timeout: int = 30) -> bool:
|
||||||
"""Poll sandbox health endpoint until ready or timeout.
|
"""Poll sandbox health endpoint until ready or timeout.
|
||||||
|
|
||||||
@ -26,14 +52,16 @@ def wait_for_sandbox_ready(sandbox_url: str, timeout: int = 30) -> bool:
|
|||||||
True if sandbox is ready, False otherwise.
|
True if sandbox is ready, False otherwise.
|
||||||
"""
|
"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while time.time() - start_time < timeout:
|
with requests.Session() as session:
|
||||||
try:
|
session.trust_env = sandbox_http_trust_env(sandbox_url)
|
||||||
response = requests.get(f"{sandbox_url}/v1/sandbox", timeout=5)
|
while time.time() - start_time < timeout:
|
||||||
if response.status_code == 200:
|
try:
|
||||||
return True
|
response = session.get(f"{sandbox_url}/v1/sandbox", timeout=5)
|
||||||
except requests.exceptions.RequestException:
|
if response.status_code == 200:
|
||||||
pass
|
return True
|
||||||
time.sleep(1)
|
except requests.exceptions.RequestException:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -47,7 +75,7 @@ async def wait_for_sandbox_ready_async(sandbox_url: str, timeout: int = 30, poll
|
|||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
deadline = loop.time() + timeout
|
deadline = loop.time() + timeout
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=5) as client:
|
async with httpx.AsyncClient(timeout=5, trust_env=sandbox_http_trust_env(sandbox_url)) as client:
|
||||||
while True:
|
while True:
|
||||||
remaining = deadline - loop.time()
|
remaining = deadline - loop.time()
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
|
|||||||
@ -7,6 +7,47 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_sandbox_client_bypasses_environment_proxy():
|
||||||
|
"""Local sandbox API calls must not inherit HTTP_PROXY (#3441)."""
|
||||||
|
from deerflow.community.aio_sandbox.aio_sandbox import AioSandbox
|
||||||
|
|
||||||
|
sentinel_httpx = MagicMock()
|
||||||
|
with (
|
||||||
|
patch("deerflow.community.aio_sandbox.aio_sandbox.httpx.Client", return_value=sentinel_httpx) as client_cls,
|
||||||
|
patch("deerflow.community.aio_sandbox.aio_sandbox.AioSandboxClient") as sdk_cls,
|
||||||
|
):
|
||||||
|
AioSandbox(id="test-sandbox", base_url="http://host.docker.internal:8080")
|
||||||
|
|
||||||
|
client_cls.assert_called_once_with(timeout=600, follow_redirects=True, trust_env=False)
|
||||||
|
sdk_cls.assert_called_once_with(
|
||||||
|
base_url="http://host.docker.internal:8080",
|
||||||
|
timeout=600,
|
||||||
|
httpx_client=sentinel_httpx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"base_url",
|
||||||
|
[
|
||||||
|
"https://sandbox.example.com",
|
||||||
|
"http://8.8.8.8:8080",
|
||||||
|
"http://[2606:4700:4700::1111]:8080",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_external_sandbox_client_keeps_environment_proxy_support(base_url: str):
|
||||||
|
"""Externally hosted sandbox URLs retain the SDK's default proxy behavior."""
|
||||||
|
from deerflow.community.aio_sandbox.aio_sandbox import AioSandbox
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("deerflow.community.aio_sandbox.aio_sandbox.httpx.Client") as client_cls,
|
||||||
|
patch("deerflow.community.aio_sandbox.aio_sandbox.AioSandboxClient") as sdk_cls,
|
||||||
|
):
|
||||||
|
AioSandbox(id="test-sandbox", base_url=base_url)
|
||||||
|
|
||||||
|
client_cls.assert_not_called()
|
||||||
|
sdk_cls.assert_called_once_with(base_url=base_url, timeout=600)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def sandbox():
|
def sandbox():
|
||||||
"""Create an AioSandbox with a mocked client."""
|
"""Create an AioSandbox with a mocked client."""
|
||||||
|
|||||||
@ -8,11 +8,20 @@ from deerflow.community.aio_sandbox import backend as readiness
|
|||||||
|
|
||||||
|
|
||||||
class _FakeAsyncClient:
|
class _FakeAsyncClient:
|
||||||
def __init__(self, *, responses: list[object], calls: list[str], timeout: float, request_timeouts: list[float] | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
responses: list[object],
|
||||||
|
calls: list[str],
|
||||||
|
timeout: float,
|
||||||
|
request_timeouts: list[float] | None = None,
|
||||||
|
trust_env: bool = True,
|
||||||
|
) -> None:
|
||||||
self._responses = responses
|
self._responses = responses
|
||||||
self._calls = calls
|
self._calls = calls
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._request_timeouts = request_timeouts
|
self._request_timeouts = request_timeouts
|
||||||
|
self.trust_env = trust_env
|
||||||
|
|
||||||
async def __aenter__(self) -> _FakeAsyncClient:
|
async def __aenter__(self) -> _FakeAsyncClient:
|
||||||
return self
|
return self
|
||||||
@ -41,17 +50,65 @@ class _FakeLoop:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("sandbox_url", "expected"),
|
||||||
|
[
|
||||||
|
("http://localhost:8080", False),
|
||||||
|
("http://127.0.0.1:8080", False),
|
||||||
|
("http://[::1]:8080", False),
|
||||||
|
("http://host.docker.internal:8080", False),
|
||||||
|
("http://host.containers.internal:8080", False),
|
||||||
|
("http://k3s:30001", False),
|
||||||
|
("http://10.0.0.8:8080", False),
|
||||||
|
("http://8.8.8.8:8080", True),
|
||||||
|
("http://[2606:4700:4700::1111]:8080", True),
|
||||||
|
("https://sandbox.example.com", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_sandbox_http_trust_env_only_uses_proxy_for_external_urls(sandbox_url: str, expected: bool) -> None:
|
||||||
|
assert readiness.sandbox_http_trust_env(sandbox_url) is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_for_sandbox_ready_bypasses_environment_proxy_for_docker_host(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
sessions: list[object] = []
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
trust_env = True
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
sessions.append(self)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_exc_info) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get(self, url: str, *, timeout: float):
|
||||||
|
assert url == "http://host.docker.internal:8080/v1/sandbox"
|
||||||
|
assert timeout == 5
|
||||||
|
return SimpleNamespace(status_code=200)
|
||||||
|
|
||||||
|
monkeypatch.setattr(readiness.requests, "Session", FakeSession)
|
||||||
|
|
||||||
|
assert readiness.wait_for_sandbox_ready("http://host.docker.internal:8080", timeout=1) is True
|
||||||
|
assert len(sessions) == 1
|
||||||
|
assert sessions[0].trust_env is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_wait_for_sandbox_ready_async_uses_nonblocking_polling(monkeypatch: pytest.MonkeyPatch) -> None:
|
async def test_wait_for_sandbox_ready_async_uses_nonblocking_polling(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
calls: list[str] = []
|
calls: list[str] = []
|
||||||
sleeps: list[float] = []
|
sleeps: list[float] = []
|
||||||
|
clients: list[_FakeAsyncClient] = []
|
||||||
|
|
||||||
def fake_client(*, timeout: float):
|
def fake_client(*, timeout: float, trust_env: bool):
|
||||||
return _FakeAsyncClient(
|
client = _FakeAsyncClient(
|
||||||
responses=[SimpleNamespace(status_code=503), SimpleNamespace(status_code=200)],
|
responses=[SimpleNamespace(status_code=503), SimpleNamespace(status_code=200)],
|
||||||
calls=calls,
|
calls=calls,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
trust_env=trust_env,
|
||||||
)
|
)
|
||||||
|
clients.append(client)
|
||||||
|
return client
|
||||||
|
|
||||||
async def fake_sleep(delay: float) -> None:
|
async def fake_sleep(delay: float) -> None:
|
||||||
sleeps.append(delay)
|
sleeps.append(delay)
|
||||||
@ -65,6 +122,7 @@ async def test_wait_for_sandbox_ready_async_uses_nonblocking_polling(monkeypatch
|
|||||||
|
|
||||||
assert calls == ["http://sandbox/v1/sandbox", "http://sandbox/v1/sandbox"]
|
assert calls == ["http://sandbox/v1/sandbox", "http://sandbox/v1/sandbox"]
|
||||||
assert sleeps == [0.05]
|
assert sleeps == [0.05]
|
||||||
|
assert clients[0].trust_env is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
@ -72,11 +130,12 @@ async def test_wait_for_sandbox_ready_async_retries_request_errors(monkeypatch:
|
|||||||
calls: list[str] = []
|
calls: list[str] = []
|
||||||
sleeps: list[float] = []
|
sleeps: list[float] = []
|
||||||
|
|
||||||
def fake_client(*, timeout: float):
|
def fake_client(*, timeout: float, trust_env: bool):
|
||||||
return _FakeAsyncClient(
|
return _FakeAsyncClient(
|
||||||
responses=[readiness.httpx.ConnectError("not ready"), SimpleNamespace(status_code=200)],
|
responses=[readiness.httpx.ConnectError("not ready"), SimpleNamespace(status_code=200)],
|
||||||
calls=calls,
|
calls=calls,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
trust_env=trust_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def fake_sleep(delay: float) -> None:
|
async def fake_sleep(delay: float) -> None:
|
||||||
@ -97,12 +156,13 @@ async def test_wait_for_sandbox_ready_async_clamps_request_and_sleep_to_deadline
|
|||||||
request_timeouts: list[float] = []
|
request_timeouts: list[float] = []
|
||||||
sleeps: list[float] = []
|
sleeps: list[float] = []
|
||||||
|
|
||||||
def fake_client(*, timeout: float):
|
def fake_client(*, timeout: float, trust_env: bool):
|
||||||
return _FakeAsyncClient(
|
return _FakeAsyncClient(
|
||||||
responses=[SimpleNamespace(status_code=503)],
|
responses=[SimpleNamespace(status_code=503)],
|
||||||
calls=calls,
|
calls=calls,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
request_timeouts=request_timeouts,
|
request_timeouts=request_timeouts,
|
||||||
|
trust_env=trust_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def fake_sleep(delay: float) -> None:
|
async def fake_sleep(delay: float) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user