deer-flow/backend/tests/test_aio_sandbox_local_backend.py
Storm b9a75b869d
fix(sandbox): attest lark broker mode per Pod and quarantine uncertain shell outcomes (#6436)
* fix(sandbox): attest lark broker mode per Pod and quarantine uncertain shell outcomes

Why this change:
Two fail-open paths in the AIO sandbox lifecycle. First, the lark broker
capability probe collapsed "provisioner unreachable" into "broker not
configured": one 1.5s timeout cached a negative for 300s, so new sandboxes
were created without the broker sidecar while the per-user credential
directories were still bind-mounted into the container, and existing Pods
kept being reused without their actual mode ever being read. Second, an
implicit-shell command that timed out or returned an unknown status only
marked the current client object; warm-pool reclaim and rediscovery built a
fresh client that re-entered a container whose shell state could not be
proven clean, and nothing about that ambiguity survived a Gateway restart.

What changed:
- The capability cache stores confirmed observations only, keyed per
  provisioner endpoint/auth identity; an unknown answer (timeout, malformed
  payload) raises instead of selecting credential placement, while a
  confirmed broker requirement is retained through outages. One in-flight
  probe per identity shares success and failure, and a Pod-attested mode
  contradicting the cache forces one shared refresh.
- Execution uses the mode attested for the actual sandbox:
  SandboxInfo.lark_cli_broker flows from the provisioner's atomic Pod
  observation into the client, so a probe outage delays drift detection but
  never denies reuse of an attested runtime, and never trusts an unattested
  one.
- The provisioner reports each Pod's actual broker mode, generation (Pod
  UID) and shell capacity from one read, rejects lark requests conflicting
  with its configuration (409/503 carrying a capability-refresh marker the
  Gateway uses to drop the stale cache entry), rejects conflicting or
  Terminating Pods on coordination creates while reporting Terminating as
  data to status/get/list, and answers Pod presence independently of
  Service readiness.
- A persistent quarantine (DEER_FLOW_HOME/sandbox-quarantine, keyed by
  scope/sandbox/generation) fences containers after uncertain implicit
  shell outcomes across client rebuilds and restarts; in-process reuse,
  warm reclaim, discovery, create responses and orphan reconciliation all
  pass one policy gate, and replacement runs under the existing ownership
  and teardown fences. A fenced sandbox still tracked after a failed
  release-recycle is destroyed inline on the next acquire.
- Quarantine records retire only after confirmed absence, retrying partial
  cleanup for the quarantined generation and releasing remembered ports
  under the same fences; local stop failures now propagate instead of being
  logged away, and remote destroy errors surface to callers.

Problem solved:
A provisioner blip could silently strip broker isolation from new lark
sandboxes for minutes at a time, and ambiguous shell outcomes let corrupted
default shells persist across warm reuse, rediscovery and restarts. Docs
updated: README, aio_sandbox and skills AGENTS.md.

* docs(changelog): record sandbox attestation and quarantine fix (#6436)

* fix(sandbox): preserve active leases and fail closed on unattested broker modes

Concurrent uploads could recycle a healthy AIO runtime while another holder
was creating or cleaning up a command session. Older provisioner responses
could also trigger destructive replacement before failing admission.

- Distinguish pending session creation from ambiguous outcomes and reserve
  idle teardown until execution and upload holders finish scope cleanup.
- Reject stale fork lookups and reconcile cancelled acquires without
  releasing a sandbox still borrowed by another holder.
- Default Lark broker mode to unverified and preserve unattested Pods until
  the provisioner is upgraded.
- Recognize Docker 29 absence diagnostics while matching resource names exactly.
- Preserve Gateway upload errors and cover admission and lifecycle regressions.

* fix(sandbox): scope the unverified-broker refuse path to attested providers

The fail-closed raise in _lark_cli_env_from_runtime treated every Sandbox
without a lark_cli_broker attribute as unverified, but only AioSandbox
carries the attestation — E2B, OpenSandbox, Tenki, and Boxlite have no
broker sidecar, no credential mounts, and no provisioner to upgrade, so
every lark-cli command on those providers failed with an unactionable
error.

Declare the tri-state on the Sandbox base class (defaulting to False,
the only mode brokerless providers have) so the None fail-closed path
stays scoped to AioSandbox's deliberate unverified default.

- Add lark_cli_broker: bool | None = False to the Sandbox ABC, mirroring
  the persistent_shell_sessions tri-state pattern
- Add a regression test pinning the non-broker overlay for a provider
  without the attribute

* fix(sandbox): close review gaps in broker attestation and teardown lifecycle

Address the latest #6436 review round:

- The provisioner's post-build broker-mode 409 now carries the
  capability-refresh marker, so an incompatible-mode conflict surfaced by
  idempotent re-create, attestation drift or post-create verification drops
  the Gateway's cached capability observation instead of repeating the same
  409 for the full negative-cache TTL.
- AioSandboxProvider.destroy() returns the _destroy_tracked outcome: False
  when teardown is deferred because an execution/upload holder or a peer
  teardown is still active, so callers can no longer mistake a deferred
  destroy for a completed one.
- A live replacement under the same logical ID proves the fenced old
  generations are absent; retire_replaced_generations prunes only those
  stale records so they cannot strand future creates, while the live
  generation's fence and unversioned crash fences are never touched.
- SandboxInfo.from_dict keeps a missing lark_cli_broker attestation
  unverified (None) instead of silently rehydrating as attested non-broker.
- The restricted-network inspect path shares the strict
  _is_no_such_network_error parser, closing the substring loophole for
  context-name collisions and wrong-target errors.

* docs(sandbox): condense lease notes to keep the AGENTS.md chain under the size gate

The three lease notes added in 26c307f3 pushed the effective guidance chain
to 98,312 bytes — 8 over the 98,304 hard limit enforced by agent-guidance
(AG002). Fold them into two lines carrying the same rules.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-10 07:56:09 +08:00

3196 lines
126 KiB
Python

import ast
import json
import logging
import os
import socket
import subprocess
import time
from pathlib import Path
from types import SimpleNamespace
import pytest
from deerflow.community.aio_sandbox import local_backend as local_backend_module
from deerflow.community.aio_sandbox.local_backend import (
LocalContainerBackend,
_ContainerInspection,
_docker_server_is_desktop,
_format_container_command_for_log,
_format_container_mount,
_NetworkInspection,
_redact_container_command_for_log,
_resolve_docker_bind_host,
)
from deerflow.community.aio_sandbox.sandbox_info import SandboxInfo
from deerflow.utils.network import get_free_port, release_port
def test_sandbox_info_does_not_serialize_or_repr_relay_credentials():
info = SandboxInfo(
sandbox_id="sandbox-id",
sandbox_url="http://localhost:8080",
request_headers={"X-DeerFlow-Relay-Token": "secret-token"},
requires_replacement=True,
)
assert "request_headers" not in info.to_dict()
assert "requires_replacement" not in info.to_dict()
assert "secret-token" not in repr(info)
def test_format_container_mount_uses_mount_syntax_for_docker_windows_paths():
args = _format_container_mount("docker", "D:/deer-flow/backend/.deer-flow/threads", "/mnt/threads", False)
assert args == [
"--mount",
"type=bind,src=D:/deer-flow/backend/.deer-flow/threads,dst=/mnt/threads",
]
def test_format_container_mount_marks_docker_readonly_mounts():
args = _format_container_mount("docker", "/host/path", "/mnt/path", True)
assert args == [
"--mount",
"type=bind,src=/host/path,dst=/mnt/path,readonly",
]
def test_format_container_mount_keeps_volume_syntax_for_apple_container():
args = _format_container_mount("container", "/host/path", "/mnt/path", True)
assert args == [
"-v",
"/host/path:/mnt/path:ro",
]
def test_redact_container_command_for_log_redacts_env_values():
redacted = _redact_container_command_for_log(
[
"docker",
"run",
"-e",
"API_KEY=secret-value",
"--env=TOKEN=token-value",
"--name",
"sandbox",
"image",
]
)
assert "API_KEY=<redacted>" in redacted
assert "--env=TOKEN=<redacted>" in redacted
assert "secret-value" not in " ".join(redacted)
assert "token-value" not in " ".join(redacted)
def test_redact_container_command_for_log_keeps_inherited_env_names():
redacted = _redact_container_command_for_log(
[
"docker",
"run",
"-e",
"API_KEY",
"--env=TOKEN",
"--name",
"sandbox",
"image",
]
)
assert redacted == [
"docker",
"run",
"-e",
"API_KEY",
"--env=TOKEN",
"--name",
"sandbox",
"image",
]
def test_format_container_command_for_log_uses_windows_quoting(monkeypatch):
monkeypatch.setattr(os, "name", "nt")
command = _format_container_command_for_log(["docker", "run", "--name", "sandbox one", "image"])
assert command == 'docker run --name "sandbox one" image'
def test_start_container_logs_redacted_env_values(monkeypatch, caplog):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={"API_KEY": "secret-value", "NORMAL": "visible-value"},
)
monkeypatch.setattr(backend, "_runtime", "docker")
captured_cmd: list[str] = []
def fake_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return SimpleNamespace(stdout="container-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
with caplog.at_level(logging.INFO, logger="deerflow.community.aio_sandbox.local_backend"):
backend._start_container("sandbox-test", 18080)
joined_cmd = " ".join(captured_cmd)
assert "API_KEY=secret-value" in joined_cmd
assert "NORMAL=visible-value" in joined_cmd
log_output = "\n".join(record.getMessage() for record in caplog.records)
assert "API_KEY=<redacted>" in log_output
assert "NORMAL=<redacted>" in log_output
assert "secret-value" not in log_output
assert "visible-value" not in log_output
def test_restricted_network_requires_docker_engine_28(monkeypatch):
monkeypatch.setattr(LocalContainerBackend, "_detect_runtime", lambda _self: "docker")
monkeypatch.setattr(
"subprocess.run",
lambda *_args, **_kwargs: SimpleNamespace(stdout="27.5.1\n", stderr="", returncode=0),
)
with pytest.raises(RuntimeError, match="Docker Engine 28 or newer"):
LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
network_config={"mode": "isolated"},
)
@pytest.mark.parametrize(
("operating_system", "expected"),
[
('"Docker Desktop"', True),
('"Ubuntu 24.04.3 LTS"', False),
],
)
def test_docker_desktop_detection_uses_daemon_operating_system(monkeypatch, operating_system, expected):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
def fake_run(cmd, **_kwargs):
assert cmd == ["docker", "info", "--format", "{{json .OperatingSystem}}"]
return SimpleNamespace(stdout=operating_system, stderr="", returncode=0)
_docker_server_is_desktop.cache_clear()
monkeypatch.setattr("subprocess.run", fake_run)
try:
assert backend._docker_server_is_desktop() is expected
finally:
_docker_server_is_desktop.cache_clear()
def test_docker_desktop_detection_retries_after_transient_failure(monkeypatch):
"""A transient probe failure is not cached; subsequent call can detect Desktop."""
attempts = 0
def fake_run(cmd, **_kwargs):
nonlocal attempts
attempts += 1
if attempts == 1:
return SimpleNamespace(stdout="", stderr="daemon starting", returncode=1)
return SimpleNamespace(stdout='"Docker Desktop"', stderr="", returncode=0)
_docker_server_is_desktop.cache_clear()
monkeypatch.setattr("subprocess.run", fake_run)
try:
assert _docker_server_is_desktop() is False
assert _docker_server_is_desktop() is True
assert _docker_server_is_desktop() is True
assert attempts == 2
finally:
_docker_server_is_desktop.cache_clear()
def test_darwin_open_keeps_docker_to_reconcile_restricted_sandbox(monkeypatch):
commands: list[list[str]] = []
def fake_run(cmd, **_kwargs):
commands.append(cmd)
if cmd[:2] == ["docker", "ps"]:
return SimpleNamespace(stdout="sandbox-transition\n", stderr="", returncode=0)
if cmd == ["container", "--version"]:
return SimpleNamespace(stdout="container 0.7.0\n", stderr="", returncode=0)
raise AssertionError(f"unexpected command: {cmd}")
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.platform.system", lambda: "Darwin")
monkeypatch.setattr("subprocess.run", fake_run)
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
network_config={"mode": "open"},
)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda _names, **_kwargs: {
"sandbox-transition": _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "transition",
"deerflow.network_mode": "allowlist",
},
"sandbox:latest",
frozenset({"deer-flow-sandbox-net-old"}),
)
},
)
infos = backend.list_running()
assert backend.runtime == "docker"
assert [info.sandbox_id for info in infos] == ["transition"]
assert infos[0].requires_replacement is True
assert ["container", "--version"] in commands
assert any("label=deerflow.role=sandbox" in command for command in commands)
def test_darwin_open_uses_apple_container_without_managed_docker_sandboxes(monkeypatch):
commands: list[list[str]] = []
def fake_run(cmd, **_kwargs):
commands.append(cmd)
if cmd == ["container", "--version"]:
return SimpleNamespace(stdout="container 0.7.0\n", stderr="", returncode=0)
if cmd[:2] == ["docker", "ps"]:
return SimpleNamespace(stdout="other-sandbox-collision\n", stderr="", returncode=0)
raise AssertionError(f"unexpected command: {cmd}")
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.platform.system", lambda: "Darwin")
monkeypatch.setattr("subprocess.run", fake_run)
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
network_config={"mode": "open"},
)
assert backend.runtime == "container"
assert any("label=deerflow.role=sandbox" in command for command in commands)
def _restricted_backend() -> LocalContainerBackend:
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
backend._runtime = "docker"
backend._network_mode = "allowlist"
backend._network_config = {
"mode": "allowlist",
"allow_domains": ["pypi.org", "files.pythonhosted.org"],
"approval": "prompt",
"temporary_grant_ttl": 300,
"proxy_image": "proxy:latest",
}
return backend
def test_network_policy_digest_is_canonical_and_covers_effective_policy():
backend = _restricted_backend()
original = backend._network_policy_digest()
backend._network_config["allow_domains"] = ["files.pythonhosted.org", "pypi.org"]
assert backend._network_policy_digest() == original
backend._network_config["allow_domains"] = ["pypi.org"]
assert backend._network_policy_digest() != original
def test_open_create_labels_sandbox_identity_and_mode(monkeypatch):
backend = _backend_for_inspect_tests()
captured: dict[str, object] = {}
def fake_start(*_args, **kwargs):
captured.update(kwargs)
return "container-id"
monkeypatch.setattr(backend, "_start_container", fake_start)
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.get_free_port", lambda start_port=None: 18080)
backend.create(thread_id="thread", sandbox_id="labelled-open")
assert captured["labels"] == {
"deerflow.sandbox_id": "labelled-open",
"deerflow.role": "sandbox",
"deerflow.network_mode": "open",
}
def test_create_internal_network_isolates_both_gateway_families_and_labels_policy(monkeypatch):
backend = _restricted_backend()
commands: list[list[str]] = []
monkeypatch.setattr(backend, "_inspect_network", lambda _name: None)
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(stdout="network-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._create_internal_network("sandbox-network", "sandbox-id")
create = commands[0]
assert "com.docker.network.bridge.gateway_mode_ipv4=isolated" in create
assert "com.docker.network.bridge.gateway_mode_ipv6=isolated" in create
assert f"deerflow.network_policy_digest={backend._network_policy_digest()}" in create
def test_create_egress_network_is_per_sandbox_and_disables_inter_container_traffic(monkeypatch):
backend = _restricted_backend()
commands: list[list[str]] = []
monkeypatch.setattr(backend, "_inspect_network", lambda _name: None)
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(stdout="network-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._create_egress_network("sandbox-egress", "sandbox-id")
create = commands[0]
assert "--internal" not in create
assert "com.docker.network.bridge.enable_icc=false" in create
assert "deerflow.role=egress-network" in create
assert f"deerflow.network_policy_digest={backend._network_policy_digest()}" in create
def test_restricted_resource_status_requires_matching_policy_image_and_network():
backend = _restricted_backend()
sandbox_id = "existing"
container_name = "sandbox-existing"
proxy_name, network_name = backend._resource_names(sandbox_id)
egress_network_name = backend._egress_network_name(sandbox_id)
inspections = {
container_name: _ContainerInspection(
created_at=1.0,
host_port=None,
labels=backend._restricted_labels(sandbox_id, "sandbox"),
image="sandbox:latest",
networks=frozenset({network_name}),
),
proxy_name: _ContainerInspection(
created_at=1.0,
host_port=18080,
labels=backend._restricted_labels(sandbox_id, "network-proxy"),
image="proxy:latest",
networks=frozenset({egress_network_name, network_name}),
relay_token="test-relay-token-that-is-at-least-32-bytes",
),
}
network = _NetworkInspection(
driver="bridge",
internal=True,
labels=backend._restricted_labels(sandbox_id, "network"),
options={
"com.docker.network.bridge.gateway_mode_ipv4": "isolated",
"com.docker.network.bridge.gateway_mode_ipv6": "isolated",
},
)
egress_network = _NetworkInspection(
driver="bridge",
internal=False,
labels=backend._restricted_labels(sandbox_id, "egress-network"),
options={"com.docker.network.bridge.enable_icc": "false"},
)
backend._inspect_network = lambda name: network if name == network_name else egress_network
assert backend._restricted_resources_status(sandbox_id, inspections=inspections) == "compatible"
compatible_proxy = inspections[proxy_name]
inspections[proxy_name] = _ContainerInspection(
created_at=compatible_proxy.created_at,
host_port=compatible_proxy.host_port,
labels=compatible_proxy.labels,
image=compatible_proxy.image,
networks=compatible_proxy.networks,
)
assert backend._restricted_resources_status(sandbox_id, inspections=inspections) == "mismatch"
inspections[proxy_name] = compatible_proxy
egress_network = _NetworkInspection(
driver="bridge",
internal=False,
labels=backend._restricted_labels(sandbox_id, "egress-network"),
options={"com.docker.network.bridge.enable_icc": "true"},
)
assert backend._restricted_resources_status(sandbox_id, inspections=inspections) == "mismatch"
egress_network = _NetworkInspection(
driver="bridge",
internal=False,
labels=backend._restricted_labels(sandbox_id, "egress-network"),
options={"com.docker.network.bridge.enable_icc": "false"},
)
inspections[proxy_name] = _ContainerInspection(
created_at=1.0,
host_port=18080,
labels={**backend._restricted_labels(sandbox_id, "network-proxy"), "deerflow.network_policy_digest": "stale"},
image="proxy:latest",
networks=frozenset({egress_network_name, network_name}),
relay_token="test-relay-token-that-is-at-least-32-bytes",
)
assert backend._restricted_resources_status(sandbox_id, inspections=inspections) == "mismatch"
def test_restricted_sandbox_has_no_published_port_and_forces_proxy_env(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={"HTTP_PROXY": "http://operator-proxy:3128"},
)
monkeypatch.setattr(backend, "_runtime", "docker")
captured_cmd: list[str] = []
def fake_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return SimpleNamespace(stdout="container-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._start_container(
"sandbox-test",
18080,
network_override="deer-flow-sandbox-net-test",
publish_port=False,
extra_environment={"HTTP_PROXY": "http://deer-flow-netproxy-test:3128"},
)
assert "-p" not in captured_cmd
assert captured_cmd[captured_cmd.index("--network") + 1] == "deer-flow-sandbox-net-test"
proxy_values = [captured_cmd[index + 1] for index, value in enumerate(captured_cmd) if value == "-e" and captured_cmd[index + 1].startswith("HTTP_PROXY=")]
assert proxy_values[-1] == "HTTP_PROXY=http://deer-flow-netproxy-test:3128"
def test_restricted_start_configures_shell_and_aio_browser_proxy(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
backend._network_mode = "allowlist"
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id: "missing")
monkeypatch.setattr(backend, "_create_internal_network", lambda _name, _sandbox_id: None)
monkeypatch.setattr(backend, "_create_egress_network", lambda _name, _sandbox_id: None)
monkeypatch.setattr(backend, "_start_network_proxy", lambda *_args: None)
captured: dict[str, object] = {}
def fake_start(*_args, **kwargs):
captured.update(kwargs)
return "container-id"
monkeypatch.setattr(backend, "_start_container", fake_start)
assert (
backend._start_restricted_sandbox(
"id",
"sandbox-id",
18080,
None,
config_mount_exclusion_root=None,
relay_token="test-relay-token",
)
== "container-id"
)
proxy_name, network_name = backend._resource_names("id")
assert captured["network_override"] == network_name
assert captured["publish_port"] is False
environment = captured["extra_environment"]
assert environment["HTTPS_PROXY"] == f"http://{proxy_name}:3128"
assert environment["ALL_PROXY"] == f"http://{proxy_name}:3128"
assert environment["PROXY_SERVER"] == f"{proxy_name}:3128"
def test_restricted_start_refuses_to_remove_resources_with_stale_policy(monkeypatch):
backend = _restricted_backend()
cleaned: list[str] = []
created: list[tuple[str, str]] = []
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id: "mismatch")
monkeypatch.setattr(backend, "_cleanup_restricted_resources", cleaned.append)
monkeypatch.setattr(backend, "_create_internal_network", lambda name, sandbox_id: created.append((name, sandbox_id)))
monkeypatch.setattr(backend, "_create_egress_network", lambda name, sandbox_id: created.append((name, sandbox_id)))
monkeypatch.setattr(backend, "_start_network_proxy", lambda *_args: None)
monkeypatch.setattr(backend, "_start_container", lambda *_args, **_kwargs: "container-id")
with pytest.raises(RuntimeError, match="requires ownership-fenced replacement"):
backend._start_restricted_sandbox(
"stale",
"sandbox-stale",
18080,
None,
config_mount_exclusion_root=None,
relay_token="test-relay-token",
)
assert cleaned == []
assert created == []
def test_network_proxy_uses_read_only_root_and_bounded_policy_storage(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
backend._network_mode = "allowlist"
backend._network_config = {
"mode": "allowlist",
"allow_domains": [],
"approval": "prompt",
"proxy_image": "proxy:latest",
}
commands: list[list[str]] = []
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(stdout="proxy-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._start_network_proxy(
"proxy-name",
"network-name",
"egress-network-name",
"sandbox-name",
18080,
"sandbox-id",
"test-relay-token",
)
create = commands[0]
assert create[create.index("--network") + 1] == "egress-network-name"
assert "bridge" not in create
assert "--read-only" in create
assert create[create.index("--tmpfs") + 1] == "/tmp:rw,noexec,nosuid,size=16m"
assert "--cap-drop=ALL" in create
assert "no-new-privileges" in create
assert "DEERFLOW_RELAY_TOKEN=test-relay-token" in create
def test_start_container_filters_nested_config_mounts_for_policy_scoped_skills(
monkeypatch,
):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[
SimpleNamespace(
host_path="/host/excluded-skill",
container_path="/mnt/skills/public/excluded-skill",
read_only=True,
),
SimpleNamespace(
host_path="/host/unrelated",
container_path="/mnt/unrelated",
read_only=True,
),
SimpleNamespace(
host_path="/host/sibling",
container_path="/mnt/skills-extra",
read_only=True,
),
],
environment={},
)
monkeypatch.setattr(backend, "_runtime", "docker")
captured_cmd: list[str] = []
def fake_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return SimpleNamespace(stdout="container-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._start_container(
"sandbox-test",
18080,
extra_mounts=[
(
"/host/thread-view/public",
"/mnt/skills/public",
True,
)
],
config_mount_exclusion_root="/mnt/skills",
)
command = " ".join(captured_cmd)
assert "/host/excluded-skill" not in command
assert "/host/unrelated" in command
assert "/host/sibling" in command
assert "/host/thread-view/public" in command
def _capture_start_container_command(monkeypatch, backend: LocalContainerBackend, runtime: str = "docker") -> list[str]:
monkeypatch.setattr(backend, "_runtime", runtime)
captured_cmd: list[str] = []
def fake_run(cmd, **kwargs):
captured_cmd.extend(cmd)
return SimpleNamespace(stdout="container-id\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._start_container("sandbox-test", 18080)
return captured_cmd
def test_resolve_docker_bind_host_defaults_loopback_for_localhost(monkeypatch):
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.delenv("DEER_FLOW_SANDBOX_HOST", raising=False)
assert _resolve_docker_bind_host() == "127.0.0.1"
def test_resolve_docker_bind_host_follows_host_gateway_mapping_for_dood(monkeypatch):
"""The bind follows what host.docker.internal actually resolves to."""
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: False,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: "192.168.64.1",
)
assert _resolve_docker_bind_host() == "192.168.64.1"
@pytest.mark.parametrize(
"sandbox_host",
[
"host.docker.internal",
"gateway.docker.internal",
"Host.Docker.Internal.",
"docker.for.mac.host.internal",
"docker.for.win.localhost",
],
)
def test_resolve_docker_bind_host_uses_loopback_on_docker_desktop(monkeypatch, sandbox_host):
"""Docker Desktop cannot bind to internal VM gateway IPs, so default to 127.0.0.1."""
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", sandbox_host)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: True,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: "192.168.65.254",
)
assert _resolve_docker_bind_host() == "127.0.0.1"
def test_resolve_docker_bind_host_preserves_custom_host_on_docker_desktop(monkeypatch):
"""Custom non-loopback sandbox host on Docker Desktop binds the resolved address."""
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "desktop-box")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: True,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: "192.0.2.55",
)
assert _resolve_docker_bind_host() == "192.0.2.55"
def test_resolve_docker_bind_host_explicit_override_precedes_desktop_detection(monkeypatch):
"""Explicit DEER_FLOW_SANDBOX_BIND_HOST takes precedence even on Docker Desktop."""
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "192.0.2.10")
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: True,
)
assert _resolve_docker_bind_host() == "192.0.2.10"
def test_resolve_docker_bind_host_brackets_ipv6_host_gateway(monkeypatch):
"""An IPv6 host-gateway mapping binds the bracketed IPv6 address."""
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: False,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: "[fd00::1]",
)
assert _resolve_docker_bind_host() == "[fd00::1]"
def test_resolve_docker_bind_host_brackets_bare_ipv6_override(monkeypatch):
"""A bare IPv6 literal in the override becomes a valid Docker publish host.
Docker's ``-p`` syntax requires bracketed IPv6 literals
(``[fd00::1]:port:8080``); operators writing the escape hatch naturally
give the bare address, so it must be normalized before use.
"""
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "fd00::1")
assert _resolve_docker_bind_host() == "[fd00::1]"
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "[fd00::1]")
assert _resolve_docker_bind_host() == "[fd00::1]"
# IPv4 literals pass through unchanged.
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "192.168.64.1")
assert _resolve_docker_bind_host() == "192.168.64.1"
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "0.0.0.0")
assert _resolve_docker_bind_host() == "0.0.0.0"
def test_resolve_docker_bind_host_resolves_hostname_override(monkeypatch):
"""-p requires an IP literal as the host part, so a hostname override
resolves to the address the daemon actually maps before use."""
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "host.docker.internal")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: "192.168.64.1" if host == "host.docker.internal" else None,
)
assert _resolve_docker_bind_host() == "192.168.64.1"
def test_resolve_docker_bind_host_rejects_unresolvable_hostname_override(monkeypatch):
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "not-a-resolvable-host.invalid")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: None,
)
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_BIND_HOST"):
_resolve_docker_bind_host()
def test_resolve_docker_bind_host_uses_discovered_bridge_gateway_when_resolution_fails(monkeypatch):
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: False,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: None,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_bridge_gateway_ip",
lambda: "192.168.64.1",
)
assert _resolve_docker_bind_host() == "192.168.64.1"
def test_resolve_docker_bind_host_falls_back_to_static_bridge_gateway(monkeypatch):
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_server_is_desktop",
lambda: False,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: None,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_bridge_gateway_ip",
lambda: None,
)
assert _resolve_docker_bind_host() == "172.17.0.1"
def test_resolve_docker_bind_host_uses_ipv6_loopback_for_ipv6_sandbox_host(monkeypatch):
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "[::1]")
assert _resolve_docker_bind_host() == "[::1]"
def test_resolve_docker_bind_host_logs_selected_bind_reason(caplog):
with caplog.at_level(logging.DEBUG, logger="deerflow.community.aio_sandbox.local_backend"):
assert _resolve_docker_bind_host(sandbox_host="localhost", bind_host="") == "127.0.0.1"
messages = "\n".join(record.getMessage() for record in caplog.records)
assert "Docker sandbox bind: 127.0.0.1 (loopback default)" in messages
def test_resolve_docker_bind_host_allows_explicit_override(monkeypatch):
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "localhost")
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "192.0.2.10")
assert _resolve_docker_bind_host() == "192.0.2.10"
def test_resolve_docker_bind_host_allows_restoring_legacy_broad_bind(monkeypatch):
"""DEER_FLOW_SANDBOX_BIND_HOST=0.0.0.0 restores the pre-hardening bind."""
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "0.0.0.0")
assert _resolve_docker_bind_host() == "0.0.0.0"
def _clear_hardening_env(monkeypatch):
for var in (
"DEER_FLOW_SANDBOX_HOST",
"DEER_FLOW_SANDBOX_BIND_HOST",
"DEER_FLOW_SANDBOX_SECCOMP_UNCONFINED",
"DEER_FLOW_SANDBOX_SECCOMP_PROFILE",
"DEER_FLOW_SANDBOX_MEMORY",
"DEER_FLOW_SANDBOX_CPUS",
"DEER_FLOW_SANDBOX_PIDS_LIMIT",
"DEER_FLOW_SANDBOX_CONTAINER_USER",
"DEER_FLOW_SANDBOX_NETWORK",
"DEER_FLOW_SANDBOX_IMAGE_STARTUP_CAPS",
):
monkeypatch.delenv(var, raising=False)
def test_start_container_binds_local_docker_port_to_loopback_by_default(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
monkeypatch.delenv("DEER_FLOW_SANDBOX_HOST", raising=False)
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("-p") + 1] == "127.0.0.1:18080:8080"
def test_start_container_brackets_bare_ipv6_bind_override(monkeypatch):
"""A bare IPv6 override reaches -p as a bracketed, valid publish host."""
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "fd00::1")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("-p") + 1] == "[fd00::1]:18080:8080"
def test_start_container_binds_dood_port_to_bridge_gateway(monkeypatch):
"""Force resolver failure so host DNS cannot bypass the bridge-gateway fallback."""
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "host.docker.internal")
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._resolve_sandbox_host_address",
lambda host: None,
)
monkeypatch.setattr(
"deerflow.community.aio_sandbox.local_backend._docker_bridge_gateway_ip",
lambda: "172.17.0.1",
)
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("-p") + 1] == "172.17.0.1:18080:8080"
def test_start_container_binds_ipv6_sandbox_host_to_ipv6_loopback(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", "[::1]")
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("-p") + 1] == "[::1]:18080:8080"
def test_start_container_keeps_apple_container_port_format(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "127.0.0.1")
captured_cmd = _capture_start_container_command(monkeypatch, backend, runtime="container")
assert captured_cmd[captured_cmd.index("-p") + 1] == "18080:8080"
def test_start_container_hardens_docker_run_by_default(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert "--cap-drop=ALL" in captured_cmd
# The shipped image's entrypoint starts as root, creates the gem user,
# chowns /opt/jupyter, prepares /run/user/1000 with chmod, and drops to
# that user via su. CHOWN/FOWNER/SETUID/SETGID must survive the drop or
# the container exits before readiness. The root nginx master also writes
# gem-owned logs under /var/log/nginx for the container's lifetime, which
# needs DAC_OVERRIDE.
cap_adds = [arg.split("=", 1)[1] for arg in captured_cmd if arg.startswith("--cap-add=")]
assert cap_adds == ["CHOWN", "FOWNER", "SETUID", "SETGID", "DAC_OVERRIDE"]
security_opts = [captured_cmd[i + 1] for i, arg in enumerate(captured_cmd) if arg == "--security-opt"]
assert "no-new-privileges" in security_opts
# The shipped AIO image needs seccomp=unconfined for its Chromium
# browser (upstream FAQ), so that option stays the default; the
# hardening that does not break the shipped image is kept.
assert "seccomp=unconfined" in security_opts
assert captured_cmd[captured_cmd.index("--memory") + 1] == "2g"
assert captured_cmd[captured_cmd.index("--cpus") + 1] == "2"
assert captured_cmd[captured_cmd.index("--pids-limit") + 1] == "512"
# Opt-in-only knobs stay absent unless explicitly configured.
assert "--user" not in captured_cmd
assert "--network" not in captured_cmd
def test_start_container_seccomp_can_opt_out_to_default_profile(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_SECCOMP_UNCONFINED", "0")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
security_opts = [captured_cmd[i + 1] for i, arg in enumerate(captured_cmd) if arg == "--security-opt"]
assert "seccomp=unconfined" not in security_opts
# The opt-out must select the built-in profile explicitly: omitting the
# option would inherit the daemon's (possibly unconfined) default.
assert "seccomp=builtin" in security_opts
assert "no-new-privileges" in security_opts
def test_start_container_seccomp_profile_env_selects_custom_profile(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_SECCOMP_PROFILE", "/etc/docker/chromium-seccomp.json")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
security_opts = [captured_cmd[i + 1] for i, arg in enumerate(captured_cmd) if arg == "--security-opt"]
assert "seccomp=/etc/docker/chromium-seccomp.json" in security_opts
assert "seccomp=unconfined" not in security_opts
assert "no-new-privileges" in security_opts
def test_resolve_sandbox_host_address_formats_and_filters(monkeypatch):
import socket as socket_module
def fake_getaddrinfo(host, port):
if host == "v4host":
return [(socket_module.AF_INET, None, None, "", ("203.0.113.7", 0))]
if host == "v6host":
return [(socket_module.AF_INET6, None, None, "", ("fd00::1%eth0", 0, 0, 0))]
if host == "wildcard":
return [(socket_module.AF_INET, None, None, "", ("0.0.0.0", 0))]
raise OSError("no such host")
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.socket.getaddrinfo", fake_getaddrinfo)
from deerflow.community.aio_sandbox.local_backend import _resolve_sandbox_host_address
assert _resolve_sandbox_host_address("v4host") == "203.0.113.7"
# zone ids are stripped and IPv6 is bracketed for docker -p syntax
assert _resolve_sandbox_host_address("v6host") == "[fd00::1]"
# wildcard resolutions are not bindable choices
assert _resolve_sandbox_host_address("wildcard") is None
assert _resolve_sandbox_host_address("unknown.invalid") is None
def test_start_container_resource_limits_env_override(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_MEMORY", "4g")
monkeypatch.setenv("DEER_FLOW_SANDBOX_CPUS", "4")
monkeypatch.setenv("DEER_FLOW_SANDBOX_PIDS_LIMIT", "1024")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("--memory") + 1] == "4g"
assert captured_cmd[captured_cmd.index("--cpus") + 1] == "4"
assert captured_cmd[captured_cmd.index("--pids-limit") + 1] == "1024"
def test_start_container_resource_limits_can_be_disabled(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_MEMORY", "0")
monkeypatch.setenv("DEER_FLOW_SANDBOX_CPUS", "none")
monkeypatch.setenv("DEER_FLOW_SANDBOX_PIDS_LIMIT", "0")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert "--memory" not in captured_cmd
assert "--cpus" not in captured_cmd
assert "--pids-limit" not in captured_cmd
def test_start_container_passes_through_user_and_network(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_CONTAINER_USER", "1000:1000")
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "deer-flow-sandbox-egress")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("--user") + 1] == "1000:1000"
assert captured_cmd[captured_cmd.index("--network") + 1] == "deer-flow-sandbox-egress"
def test_start_container_rejects_host_networking(monkeypatch):
"""host mode discards -p/--publish, voiding the hardened bind and
re-exposing the unauthenticated exec API on the host's interfaces."""
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "host")
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_NETWORK"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_rejects_shared_container_network_namespace(monkeypatch):
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "container:gateway")
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_NETWORK"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_rejects_none_network(monkeypatch):
"""The none driver leaves the container loopback-only, so the published
sandbox API port cannot receive traffic: readiness would time out and
every acquisition would fail. Fail fast at start-up instead."""
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "none")
with pytest.raises(RuntimeError, match="loopback-only"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_does_not_add_docker_hardening_to_apple_container(monkeypatch):
"""Apple Container's CLI does not support the Docker hardening flags."""
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_BIND_HOST", "127.0.0.1")
captured_cmd = _capture_start_container_command(monkeypatch, backend, runtime="container")
assert "--cap-drop=ALL" not in captured_cmd
assert "--security-opt" not in captured_cmd
assert "--memory" not in captured_cmd
assert "--cpus" not in captured_cmd
assert "--pids-limit" not in captured_cmd
def _backend_for_inspect_tests() -> LocalContainerBackend:
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
backend._runtime = "docker"
return backend
def test_is_container_running_false_when_container_missing(monkeypatch):
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
return SimpleNamespace(stdout="", stderr="Error: No such object: sandbox-missing", returncode=1)
monkeypatch.setattr("subprocess.run", fake_run)
assert backend._is_container_running("sandbox-missing") is False
def test_is_container_running_raises_on_runtime_error(monkeypatch):
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
return SimpleNamespace(stdout="", stderr="Cannot connect to the Docker daemon", returncode=1)
monkeypatch.setattr("subprocess.run", fake_run)
with pytest.raises(RuntimeError, match="Failed to inspect container sandbox-busy"):
backend._is_container_running("sandbox-busy")
def test_is_container_running_raises_on_timeout(monkeypatch):
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
raise subprocess.TimeoutExpired(cmd=cmd, timeout=kwargs["timeout"])
monkeypatch.setattr("subprocess.run", fake_run)
with pytest.raises(RuntimeError, match="Timed out checking container sandbox-timeout"):
backend._is_container_running("sandbox-timeout")
def test_discover_returns_none_when_runtime_check_fails(monkeypatch):
"""A transient daemon error during discovery must fall through to create, not fail acquire."""
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
return SimpleNamespace(stdout="", stderr="Cannot connect to the Docker daemon", returncode=1)
monkeypatch.setattr("subprocess.run", fake_run)
assert backend.discover("sandbox-blip") is None
def test_discover_returns_none_when_runtime_check_times_out(monkeypatch):
"""An inspect timeout during discovery must not propagate out of discover()."""
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
raise subprocess.TimeoutExpired(cmd=cmd, timeout=kwargs["timeout"])
monkeypatch.setattr("subprocess.run", fake_run)
assert backend.discover("sandbox-timeout") is None
def test_discover_replaces_container_with_insufficient_shell_capacity(monkeypatch):
backend = _backend_for_inspect_tests()
backend._environment["MAX_SHELL_SESSIONS"] = "13"
container_name = "sandbox-existing"
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
created_at=1.0,
host_port=18080,
labels={
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "existing",
"deerflow.network_mode": "open",
},
image="sandbox:latest",
networks=frozenset({"bridge"}),
max_shell_sessions=10,
)
},
)
info = backend.discover("existing")
assert info is not None
assert info.requires_replacement is True
assert info.sandbox_url == ""
def test_list_running_marks_insufficient_shell_capacity_for_fenced_replacement(monkeypatch):
backend = _backend_for_inspect_tests()
backend._environment["MAX_SHELL_SESSIONS"] = "13"
container_name = "sandbox-existing"
def fake_run(cmd, **kwargs):
assert cmd[:2] == ["docker", "ps"]
return SimpleNamespace(stdout=f"{container_name}\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
created_at=1.0,
host_port=18080,
labels={
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "existing",
"deerflow.network_mode": "open",
},
image="sandbox:latest",
networks=frozenset({"bridge"}),
max_shell_sessions=10,
)
},
)
infos = backend.list_running()
assert len(infos) == 1
assert infos[0].requires_replacement is True
assert infos[0].sandbox_url == ""
def test_restricted_discovery_uses_proxy_relay_port(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
container_name = "sandbox-existing"
proxy_name, _ = backend._resource_names("existing")
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
inspections = {
container_name: _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "existing",
"deerflow.network_mode": "allowlist",
},
"sandbox:latest",
frozenset(),
),
proxy_name: _ContainerInspection(
1.0,
18080,
{},
"proxy:latest",
frozenset(),
"test-relay-token-that-is-at-least-32-bytes",
),
}
monkeypatch.setattr(backend, "_batch_inspect", lambda *_args, **_kwargs: inspections)
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id, **_kwargs: "compatible")
readiness: list[dict[str, object]] = []
def fake_ready(_url, **kwargs):
readiness.append(kwargs)
return True
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.wait_for_sandbox_ready", fake_ready)
info = backend.discover("existing")
assert info is not None
assert info.container_name == "sandbox-existing"
assert info.sandbox_url == "http://localhost:18080"
assert info.request_headers == {"X-DeerFlow-Relay-Token": "test-relay-token-that-is-at-least-32-bytes"}
assert readiness == [{"timeout": 5, "headers": info.request_headers}]
def test_restricted_discovery_reports_stale_policy_without_removing_resources(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
cleaned: list[str] = []
container_name = "sandbox-stale"
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "stale",
"deerflow.network_mode": "allowlist",
},
"sandbox:latest",
frozenset(),
)
},
)
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id, **_kwargs: "mismatch")
monkeypatch.setattr(backend, "_cleanup_restricted_resources", cleaned.append)
info = backend.discover("stale")
assert info is not None
assert info.sandbox_id == "stale"
assert info.container_name == "sandbox-stale"
assert info.requires_replacement is True
assert cleaned == []
def test_restricted_discovery_reports_legacy_open_sandbox_for_fenced_replacement(monkeypatch):
backend = _restricted_backend()
container_name = "sandbox-legacy-open"
inspected_batches: list[list[str]] = []
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
def fake_batch_inspect(names, **_kwargs):
inspected_batches.append(list(names))
return {
container_name: _ContainerInspection(
1.0,
18080,
{},
"sandbox:latest",
frozenset({"bridge"}),
)
}
monkeypatch.setattr(backend, "_batch_inspect", fake_batch_inspect)
info = backend.discover("legacy-open")
assert info is not None
assert info.requires_replacement is True
assert info.sandbox_url == ""
assert inspected_batches == [[container_name]]
def test_restricted_discovery_reports_labelled_open_sandbox_for_fenced_replacement(monkeypatch):
backend = _restricted_backend()
container_name = "sandbox-labelled-open"
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
1.0,
18080,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "labelled-open",
"deerflow.network_mode": "open",
},
"sandbox:latest",
frozenset({"bridge"}),
)
},
)
monkeypatch.setattr(
backend,
"_restricted_resources_status",
lambda *_args, **_kwargs: pytest.fail("a mode mismatch must be reported before restricted resource inspection"),
)
info = backend.discover("labelled-open")
assert info is not None
assert info.requires_replacement is True
assert info.sandbox_url == ""
def test_open_discovery_reports_restricted_sandbox_for_fenced_replacement(monkeypatch):
backend = _backend_for_inspect_tests()
container_name = "sandbox-old-restricted"
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "old-restricted",
"deerflow.network_mode": "allowlist",
},
"sandbox:latest",
frozenset({"deer-flow-sandbox-net-old"}),
)
},
)
info = backend.discover("old-restricted")
assert info is not None
assert info.requires_replacement is True
assert info.sandbox_url == ""
def test_restricted_discovery_leaves_unlabelled_name_collision_unmanaged(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
container_name = "sandbox-foreign"
cleaned: list[str] = []
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
1.0,
None,
{},
"foreign:latest",
frozenset(),
)
},
)
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id, **_kwargs: "mismatch")
monkeypatch.setattr(backend, "_cleanup_restricted_resources", cleaned.append)
assert backend.discover("foreign") is None
assert cleaned == []
def test_restricted_discovery_leaves_unlabelled_published_foreign_image_unmanaged(monkeypatch):
backend = _restricted_backend()
container_name = "sandbox-foreign-published"
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
container_name: _ContainerInspection(
1.0,
18080,
{},
"foreign:latest",
frozenset({"bridge"}),
)
},
)
assert backend.discover("foreign-published") is None
def test_restricted_health_rejects_resources_with_stale_policy(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
monkeypatch.setattr(backend, "_is_container_running", lambda _name: True)
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id: "mismatch")
assert not backend.is_alive(
SandboxInfo(
sandbox_id="stale",
sandbox_url="http://localhost:18080",
container_name="sandbox-stale",
)
)
def test_restricted_list_reconciliation_reports_stale_policy_without_removing_resources(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
proxy_name, _ = backend._resource_names("stale")
cleaned: list[str] = []
monkeypatch.setattr(
"subprocess.run",
lambda *_args, **_kwargs: SimpleNamespace(stdout="sandbox-stale\n", stderr="", returncode=0),
)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda _names, **_kwargs: {
"sandbox-stale": _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "stale",
"deerflow.network_mode": "allowlist",
},
"sandbox:latest",
frozenset(),
),
proxy_name: _ContainerInspection(
1.0,
18080,
{"deerflow.role": "network-proxy", "deerflow.sandbox_id": "stale"},
"proxy:latest",
frozenset(),
),
},
)
monkeypatch.setattr(backend, "_restricted_resources_status", lambda _sandbox_id, **_kwargs: "mismatch")
monkeypatch.setattr(backend, "_cleanup_restricted_resources", cleaned.append)
infos = backend.list_running()
assert len(infos) == 1
assert infos[0].sandbox_id == "stale"
assert infos[0].requires_replacement is True
assert cleaned == []
def test_restricted_list_reports_legacy_open_sandbox_for_fenced_replacement(monkeypatch):
backend = _restricted_backend()
commands: list[list[str]] = []
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(stdout="sandbox-legacy-open\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda _names, **_kwargs: {
"sandbox-legacy-open": _ContainerInspection(
1.0,
18080,
{},
"sandbox:latest",
frozenset({"bridge"}),
)
},
)
infos = backend.list_running()
assert len(infos) == 1
assert infos[0].requires_replacement is True
assert infos[0].sandbox_url == ""
assert "label=deerflow.role=sandbox" not in commands[0]
def test_open_list_reports_restricted_sandbox_for_fenced_replacement(monkeypatch):
backend = _backend_for_inspect_tests()
monkeypatch.setattr(
"subprocess.run",
lambda *_args, **_kwargs: SimpleNamespace(stdout="sandbox-old-restricted\n", stderr="", returncode=0),
)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda _names, **_kwargs: {
"sandbox-old-restricted": _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": "old-restricted",
"deerflow.network_mode": "isolated",
},
"sandbox:latest",
frozenset({"deer-flow-sandbox-net-old"}),
)
},
)
infos = backend.list_running()
assert len(infos) == 1
assert infos[0].requires_replacement is True
assert infos[0].sandbox_url == ""
def test_restricted_list_running_excludes_sidecars_for_overlapping_custom_prefix(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
backend._container_prefix = "deer-flow"
sandbox_id = "live"
sandbox_name = "deer-flow-live"
proxy_name, _ = backend._resource_names(sandbox_id)
commands: list[list[str]] = []
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(
stdout=f"{sandbox_name}\n{proxy_name}\n",
stderr="",
returncode=0,
)
monkeypatch.setattr("subprocess.run", fake_run)
inspected_batches: list[list[str]] = []
def fake_batch_inspect(names, **_kwargs):
inspected_batches.append(list(names))
return {
sandbox_name: _ContainerInspection(
1.0,
None,
{
"deerflow.role": "sandbox",
"deerflow.sandbox_id": sandbox_id,
"deerflow.network_mode": "allowlist",
},
"sandbox:latest",
frozenset(),
),
proxy_name: _ContainerInspection(
1.0,
18080,
{"deerflow.role": "network-proxy", "deerflow.sandbox_id": sandbox_id},
"proxy:latest",
frozenset(),
"test-relay-token-that-is-at-least-32-bytes",
),
}
monkeypatch.setattr(backend, "_batch_inspect", fake_batch_inspect)
checked: list[str] = []
def compatible(current_sandbox_id, **_kwargs):
checked.append(current_sandbox_id)
return "compatible"
monkeypatch.setattr(backend, "_restricted_resources_status", compatible)
infos = backend.list_running()
assert [info.sandbox_id for info in infos] == [sandbox_id]
assert checked == [sandbox_id]
assert "label=deerflow.role=sandbox" not in commands[0]
sidecar_as_sandbox_id = proxy_name[len(backend._container_prefix) + 1 :]
fabricated_proxy_name, _ = backend._resource_names(sidecar_as_sandbox_id)
assert fabricated_proxy_name not in {name for batch in inspected_batches for name in batch}
def test_restricted_destroy_stops_pair_and_removes_both_networks(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "isolated"
proxy_name, network_name = backend._resource_names("existing")
egress_network_name = backend._egress_network_name("existing")
stopped: list[str] = []
commands: list[list[str]] = []
monkeypatch.setattr(backend, "_stop_container", stopped.append)
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(stdout="", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend.destroy(
SandboxInfo(
sandbox_id="existing",
sandbox_url="http://localhost:18080",
container_name="sandbox-existing",
container_id="sandbox-container-id",
)
)
assert stopped == [proxy_name, "sandbox-container-id"]
assert ["docker", "rm", "-f", proxy_name] in commands
assert ["docker", "network", "rm", network_name] in commands
assert ["docker", "network", "rm", egress_network_name] in commands
def test_open_mode_replacement_destroy_removes_restricted_sidecar_and_networks(monkeypatch):
backend = _backend_for_inspect_tests()
stopped: list[str] = []
cleaned: list[tuple[str, bool, bool]] = []
monkeypatch.setattr(backend, "_stop_container", stopped.append)
monkeypatch.setattr(backend, "_remove_restricted_proxy", lambda sandbox_id: cleaned.append((sandbox_id, False, True)))
monkeypatch.setattr(
backend,
"_cleanup_restricted_resources",
lambda sandbox_id, *, stop_sandbox=True, remove_proxy=True: cleaned.append((sandbox_id, stop_sandbox, remove_proxy)),
)
backend.destroy(
SandboxInfo(
sandbox_id="old-restricted",
sandbox_url="",
container_name="sandbox-old-restricted",
requires_replacement=True,
)
)
assert stopped == ["sandbox-old-restricted"]
assert cleaned == [("old-restricted", False, True), ("old-restricted", False, False)]
def test_deny_pending_network_policy_events_uses_atomic_proxy_command(monkeypatch):
backend = _restricted_backend()
commands: list[list[str]] = []
def fake_run(cmd, **_kwargs):
commands.append(cmd)
return SimpleNamespace(stdout="17\n", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
assert backend.deny_pending_network_policy_events("existing") is True
proxy_name, _ = backend._resource_names("existing")
assert commands == [["docker", "exec", proxy_name, "python", "/tmp/deerflow-network-proxy.py", "deny-pending"]]
def test_is_container_running_false_on_apple_container_not_found(monkeypatch):
"""Apple Container's generic "not found" is trusted when it names the container."""
backend = _backend_for_inspect_tests()
backend._runtime = "container"
def fake_run(cmd, **kwargs):
return SimpleNamespace(stdout="", stderr='Error: not found: "sandbox-apple"', returncode=1)
monkeypatch.setattr("subprocess.run", fake_run)
assert backend._is_container_running("sandbox-apple") is False
def test_is_container_running_raises_on_unrelated_not_found_error(monkeypatch):
"""Transient errors whose text contains "not found" must not be misread as a dead container."""
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
return SimpleNamespace(stdout="", stderr="Error: credential helper not found in $PATH", returncode=1)
monkeypatch.setattr("subprocess.run", fake_run)
with pytest.raises(RuntimeError, match="Failed to inspect container sandbox-busy"):
backend._is_container_running("sandbox-busy")
@pytest.mark.parametrize("runtime", ["docker", "container"])
def test_absence_does_not_treat_stopped_container_as_missing(monkeypatch, runtime):
backend = _backend_for_inspect_tests()
backend._runtime = runtime
name = "sandbox-stopped"
entry = {"Name": f"/{name}", "Id": "stopped-generation", "State": {"Running": False}} if runtime == "docker" else {"configuration": {"id": name}, "status": "stopped"}
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0))
assert backend.is_absent("stopped") is False
@pytest.mark.parametrize("runtime", ["docker", "container"])
def test_absence_requires_runtime_confirmation(monkeypatch, runtime):
backend = _backend_for_inspect_tests()
backend._runtime = runtime
name = f"{backend._container_prefix}-missing"
error = f"Error: No such container: {name}" if runtime == "docker" else f'Error: not found: "{name}"'
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout="", stderr=error, returncode=1))
assert backend.is_absent("missing") is True
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout="", stderr="daemon unavailable", returncode=1))
with pytest.raises(RuntimeError):
backend.is_absent("missing")
@pytest.mark.parametrize("operation", ["is_absent", "_is_container_running", "_stop_container"])
@pytest.mark.parametrize(
"error",
[
'Failed to initialize: unable to resolve docker endpoint: context "deerflow-container-review-missing": context not found: open /synthetic-home/.docker/contexts/meta/context/meta.json: no such file or directory',
'unable to resolve docker endpoint: context "sandbox-missing": context not found',
"Error: container runtime not found while inspecting sandbox-missing",
"OCI runtime failed for container sandbox-missing: executable file not found in $PATH",
"Error: No such object: sandbox-other",
"Error response from daemon: No such container: sandbox-missing-extra",
# Docker 29 lowercases the client-side scaffolding; the near-miss and
# case-sensitivity rules must survive the normalization.
"error: no such object: sandbox-other",
"error response from daemon: no such container: sandbox-missing-extra",
"error: no such object: Sandbox-Missing",
"error: no such object",
"error: no such image: sandbox-missing",
"prefix error: no such object: sandbox-missing",
"error: no such object: sandbox-missing suffix",
"error: no such object: sandbox-missing\nError response from daemon: permission denied",
"warning: using default context\nerror: no such object: sandbox-missing",
],
)
def test_runtime_absence_rejects_errors_not_about_the_requested_container(monkeypatch, operation, error):
backend = _backend_for_inspect_tests()
def failed(cmd, **kwargs):
if kwargs.get("check"):
raise subprocess.CalledProcessError(1, cmd, stderr=error)
return SimpleNamespace(stdout="", stderr=error, returncode=1)
monkeypatch.setattr("subprocess.run", failed)
target = "missing" if operation == "is_absent" else "sandbox-missing"
with pytest.raises((RuntimeError, subprocess.CalledProcessError)):
getattr(backend, operation)(target)
@pytest.mark.parametrize("operation", ["is_absent", "_is_container_running", "_stop_container"])
@pytest.mark.parametrize(
("runtime", "error"),
[
("docker", "Error: No such object: sandbox-missing"),
("docker", "Error response from daemon: No such container: sandbox-missing"),
# Docker CLI v29 lowercases the client-side error scaffolding
# ("error: no such object: <name>"); the name stays byte-exact.
("docker", "error: no such object: sandbox-missing"),
("docker", "error response from daemon: no such container: sandbox-missing"),
# ArgumentParser prefers ContainerizationError.errorDescription, so
# ContainerInspect's missing-resource message has no type or quotes.
("container", "Error: container not found: sandbox-missing"),
("container", 'Error: notFound: "container not found: sandbox-missing"'),
("container", 'notFound: "container not found: sandbox-missing"'),
("container", 'Error: not found: "sandbox-missing"'),
],
)
def test_runtime_absence_accepts_exact_missing_container_responses(monkeypatch, operation, runtime, error):
backend = _backend_for_inspect_tests()
backend._runtime = runtime
def missing(cmd, **kwargs):
if kwargs.get("check"):
raise subprocess.CalledProcessError(1, cmd, stderr=error)
return SimpleNamespace(stdout="[]", stderr=error, returncode=1)
monkeypatch.setattr("subprocess.run", missing)
target = "missing" if operation == "is_absent" else "sandbox-missing"
result = getattr(backend, operation)(target)
assert result is (True if operation == "is_absent" else False if operation == "_is_container_running" else None)
@pytest.mark.parametrize("state", ["created", "exited", "running", "dead"])
def test_inspect_runtime_observes_docker_generation_without_health_or_port(monkeypatch, state):
backend = _backend_for_inspect_tests()
commands = []
entry = {"Id": "immutable-docker-generation", "Name": "/sandbox-existing", "Created": "2026-04-08T01:22:50Z", "State": {"Status": state, "Running": state == "running"}}
def inspect(cmd, **kwargs):
commands.append(cmd)
assert 0 < kwargs["timeout"] <= 15
return SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", inspect)
monkeypatch.setattr(backend, "_is_container_running", lambda *_args: pytest.fail("runtime inspection must not require running"))
monkeypatch.setattr(backend, "_get_container_port", lambda *_args: pytest.fail("runtime inspection must not require a port"))
monkeypatch.setattr(local_backend_module, "wait_for_sandbox_ready", lambda *_args, **_kwargs: pytest.fail("runtime inspection must not require health"))
info = backend.inspect_runtime("existing")
assert info is not None
assert info.sandbox_id == "existing"
assert info.container_name == "sandbox-existing"
assert info.container_id == "immutable-docker-generation"
assert info.created_at == 1775611370.0
assert commands == [["docker", "inspect", "sandbox-existing"]]
def test_inspect_runtime_keeps_apple_resource_with_unknown_generation(monkeypatch):
backend = _backend_for_inspect_tests()
backend._runtime = "container"
# ManagedContainer serializes configuration.id, which is the user-selected
# logical name rather than an immutable generation; it cannot fence teardown.
entry = {"id": "sandbox-existing", "configuration": {"id": "sandbox-existing"}, "status": {"state": "stopped"}}
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0))
info = backend.inspect_runtime("existing")
assert info is not None
assert info.container_name == "sandbox-existing"
assert info.container_id is None
assert backend.is_absent("existing") is False
@pytest.mark.parametrize("stopped", [False, True])
def test_destroy_retry_releases_port_recovered_from_runtime_metadata(monkeypatch, stopped):
from deerflow.utils.network import PortAllocator
backend = _backend_for_inspect_tests()
allocator = PortAllocator()
monkeypatch.setattr(allocator, "_is_port_available", lambda port: port not in allocator._reserved_ports)
port = allocator.allocate(18080, max_range=1)
monkeypatch.setattr(local_backend_module, "release_port", allocator.release)
entry = {
"Id": "retained-generation",
"Name": "/sandbox-existing",
"NetworkSettings": {"Ports": {"8080/tcp": None if stopped else [{"HostPort": str(port)}]}},
"HostConfig": {"PortBindings": {"8080/tcp": [{"HostPort": str(port)}]}},
}
failed = True
def runtime(cmd, **kwargs):
nonlocal failed
if "inspect" in cmd:
return SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0)
if failed:
failed = False
raise subprocess.CalledProcessError(1, cmd, stderr="daemon denied stop")
return SimpleNamespace(stdout="", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
original = SandboxInfo("existing", f"http://localhost:{port}", container_id="retained-generation")
with pytest.raises(subprocess.CalledProcessError):
backend.destroy(original)
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(18080, max_range=1)
recovered = backend.inspect_runtime("existing")
assert recovered is not None
backend.destroy(recovered)
assert allocator.allocate(18080, max_range=1) == port
@pytest.mark.parametrize("proxy_missing", [False, True])
@pytest.mark.parametrize("persisted_mode", ["allowlist", "isolated"])
def test_inspect_runtime_recovers_restricted_proxy_port_without_losing_main_identity(monkeypatch, proxy_missing, persisted_mode):
backend = _backend_for_inspect_tests()
proxy_name, _ = backend._resource_names("existing")
entry = {"Id": "main-generation", "Name": "/sandbox-existing", "Config": {"Labels": {"deerflow.network_mode": persisted_mode}}}
proxy = {"Id": "proxy-generation", "Name": f"/{proxy_name}", "HostConfig": {"PortBindings": {"8080/tcp": [{"HostPort": "18080"}]}}}
def runtime(cmd, **kwargs):
if cmd[-1] == proxy_name:
return SimpleNamespace(stdout="[]" if proxy_missing else json.dumps([proxy]), stderr=f"Error: No such object: {proxy_name}" if proxy_missing else "", returncode=1 if proxy_missing else 0)
return SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
info = backend.inspect_runtime("existing")
assert info is not None
assert info.container_id == "main-generation"
assert info.sandbox_url == ("" if proxy_missing else "http://localhost:18080")
def test_inspect_runtime_propagates_unknown_proxy_port_probe(monkeypatch):
backend = _backend_for_inspect_tests()
proxy_name, _ = backend._resource_names("existing")
entry = {"Id": "main-generation", "Name": "/sandbox-existing", "Config": {"Labels": {"deerflow.network_mode": "allowlist"}}}
def runtime(cmd, **kwargs):
if cmd[-1] == proxy_name:
return SimpleNamespace(stdout="", stderr="daemon unavailable", returncode=1)
return SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
with pytest.raises(RuntimeError, match="daemon unavailable"):
backend.inspect_runtime("existing")
def test_inspect_runtime_retains_direct_binding_during_network_mode_transition(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
proxy_name, _ = backend._resource_names("existing")
entry = {"Id": "legacy-generation", "Name": "/sandbox-existing", "HostConfig": {"PortBindings": {"8080/tcp": [{"HostPort": "18080"}]}}}
def runtime(cmd, **kwargs):
if cmd[-1] == proxy_name:
return SimpleNamespace(stdout="[]", stderr=f"Error: No such object: {proxy_name}", returncode=1)
return SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
assert backend.inspect_runtime("existing").sandbox_url == "http://localhost:18080"
def test_inspect_runtime_does_not_inherit_another_generations_pending_port(monkeypatch):
backend = _backend_for_inspect_tests()
backend._pending_cleanup_ports["old-generation"] = ("existing", 18080)
entry = {"Id": "replacement-generation", "Name": "/sandbox-existing"}
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0))
info = backend.inspect_runtime("existing")
assert info.container_id == "replacement-generation"
assert info.sandbox_url == ""
assert backend._pending_cleanup_ports == {"old-generation": ("existing", 18080)}
def test_destroy_releases_port_after_both_containers_are_removed_despite_network_timeout(monkeypatch):
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
proxy_name, _ = backend._resource_names("existing")
stopped = []
monkeypatch.setattr(backend, "_stop_container", stopped.append)
def runtime(cmd, **kwargs):
if cmd[:3] == ["docker", "network", "rm"]:
raise subprocess.TimeoutExpired(cmd, 15)
return SimpleNamespace(stdout="", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
info = SandboxInfo("existing", "http://localhost:18080", container_name="sandbox-existing", container_id="main-generation")
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(info)
assert stopped == [proxy_name, "main-generation"]
assert released == [18080]
assert backend._pending_cleanup_ports == {}
def test_confirmed_absent_teardown_releases_a_timed_out_but_removed_generations_port_once(monkeypatch):
from concurrent.futures import ThreadPoolExecutor
from deerflow.utils.network import PortAllocator
backend = _backend_for_inspect_tests()
allocator = PortAllocator()
monkeypatch.setattr(allocator, "_is_port_available", lambda port: port not in allocator._reserved_ports)
port = allocator.allocate(18080, max_range=1)
other_port = allocator.allocate(18081, max_range=1)
released = []
removed = False
def release(port):
released.append(port)
allocator.release(port)
def runtime(cmd, **kwargs):
nonlocal removed
if "stop" in cmd:
if cmd[-1].startswith("deer-flow-netproxy-"):
raise subprocess.CalledProcessError(1, cmd, stderr=f"Error: No such container: {cmd[-1]}")
if cmd[-1] == "removed-generation":
removed = True
raise subprocess.TimeoutExpired(cmd, 120)
if cmd[:3] in [["docker", "rm", "-f"], ["docker", "network", "rm"]]:
return SimpleNamespace(stdout="", stderr="", returncode=0)
assert removed
return SimpleNamespace(stdout="[]", stderr="Error: No such object: sandbox-existing", returncode=1)
monkeypatch.setattr("subprocess.run", runtime)
monkeypatch.setattr(local_backend_module, "release_port", release)
for sandbox_id, generation, reserved_port in [("other", "other-generation", other_port), ("existing", "removed-generation", port)]:
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(SandboxInfo(sandbox_id, f"http://localhost:{reserved_port}", container_id=generation))
assert backend.inspect_runtime("existing") is None
assert backend.is_absent("existing") is True
assert released == [] # Metadata and presence reads never mutate reservations.
with ThreadPoolExecutor(max_workers=2) as pool:
list(pool.map(backend.complete_absent_teardown, ["existing", "existing"]))
assert released == [port]
assert allocator.allocate(18080, max_range=1) == port
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(18081, max_range=1)
backend.complete_absent_teardown("existing")
assert released == [port]
@pytest.mark.parametrize("restarted", [False, True])
def test_absent_teardown_removes_leftover_networks_and_allows_restricted_creation(monkeypatch, restarted):
backend = _restricted_backend()
restarted_backend = _restricted_backend() if restarted else None
proxy_name, network_name = backend._resource_names("existing")
networks = {network_name, backend._egress_network_name("existing")}
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
def runtime(cmd, **kwargs):
if "stop" in cmd:
if cmd[-1] == "old-generation":
raise subprocess.TimeoutExpired(cmd, 120)
raise subprocess.CalledProcessError(1, cmd, stderr=f"Error: No such container: {cmd[-1]}")
if cmd[:3] == ["docker", "rm", "-f"]:
return SimpleNamespace(stdout="", stderr=f"Error: No such container: {proxy_name}", returncode=1)
if cmd[:3] == ["docker", "network", "rm"]:
networks.discard(cmd[-1])
return SimpleNamespace(stdout=cmd[-1], stderr="", returncode=0)
if cmd[:3] == ["docker", "network", "inspect"]:
if cmd[-1] in networks:
return SimpleNamespace(stdout=json.dumps([{"Driver": "bridge", "Internal": True}]), stderr="", returncode=0)
return SimpleNamespace(stdout="[]", stderr=f"Error response from daemon: network {cmd[-1]} not found", returncode=1)
assert cmd[:2] == ["docker", "inspect"]
return SimpleNamespace(stdout="[]", stderr="\n".join(f"Error: No such container: {name}" for name in cmd[2:]), returncode=1)
monkeypatch.setattr("subprocess.run", runtime)
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(SandboxInfo("existing", "http://localhost:18080", container_id="old-generation"))
assert backend.is_absent("existing") is True
assert backend._restricted_resources_status("existing") == "mismatch"
if restarted:
backend = restarted_backend
assert backend._pending_cleanup_ports == {}
backend.complete_absent_teardown("existing")
assert backend._restricted_resources_status("existing") == "missing"
assert released == ([] if restarted else [18080])
monkeypatch.setattr(backend, "_create_internal_network", lambda *_args: None)
monkeypatch.setattr(backend, "_create_egress_network", lambda *_args: None)
monkeypatch.setattr(backend, "_start_network_proxy", lambda *_args: None)
monkeypatch.setattr(backend, "_start_container", lambda *_args, **_kwargs: "replacement-generation")
assert backend._start_restricted_sandbox("existing", "sandbox-existing", 18080, None, config_mount_exclusion_root=None, relay_token="token") == "replacement-generation"
@pytest.mark.parametrize(
"stderr, absent",
[
("Error response from daemon: network sandbox-net-x not found", True),
("error: network sandbox-net-x not found", True), # Docker 29 lowercase
("Error: No such network: sandbox-net-x", True),
# A different network's absence, a context failure, or a lookalike
# must raise instead of reading as absent.
("Error response from daemon: network sandbox-net-x-extra not found", False),
('context "sandbox-net-x": context not found', False),
("command not found: docker network inspect", False),
],
)
def test_inspect_network_uses_strict_missing_network_parser(monkeypatch, stderr, absent):
"""The non-strict inspect path shares the strict parser: context-name
collisions and wrong-target errors raise instead of reading as absent."""
backend = _restricted_backend()
monkeypatch.setattr(
"subprocess.run",
lambda *a, **k: SimpleNamespace(stdout="[]", stderr=stderr, returncode=1),
)
if absent:
assert backend._inspect_network("sandbox-net-x") is None
else:
with pytest.raises(RuntimeError):
backend._inspect_network("sandbox-net-x")
@pytest.mark.parametrize("error_kind", ["context", "wrong-target", "wrong-target-lowercase", "case-drift", "active-endpoint", "prefix", "suffix", "mixed-stderr", "empty"])
def test_absent_teardown_preserves_pending_port_on_unknown_network_cleanup_error(monkeypatch, error_kind):
backend = _restricted_backend()
backend._pending_cleanup_ports["old-generation"] = ("existing", 18080)
proxy_name, network_name = backend._resource_names("existing")
errors = {
"context": f'context "{network_name}": context not found',
"wrong-target": f"Error response from daemon: network {network_name}-extra not found",
"wrong-target-lowercase": f"error: network {network_name}-extra not found",
"case-drift": f"error: network {network_name.upper()} not found",
"active-endpoint": f"Error response from daemon: network {network_name} has active endpoints",
"prefix": f"prefix error: network {network_name} not found",
"suffix": f"error: network {network_name} not found suffix",
"mixed-stderr": f"error: network {network_name} not found\nError response from daemon: permission denied",
"empty": "",
}
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
monkeypatch.setattr(backend, "_stop_container", lambda *_args: None)
def runtime(cmd, **kwargs):
if cmd == ["docker", "rm", "-f", proxy_name]:
return SimpleNamespace(stdout="", stderr="", returncode=0)
return SimpleNamespace(stdout="", stderr=errors[error_kind], returncode=1)
monkeypatch.setattr("subprocess.run", runtime)
with pytest.raises(RuntimeError):
backend.complete_absent_teardown("existing")
assert released == []
assert backend._pending_cleanup_ports == {"old-generation": ("existing", 18080)}
@pytest.mark.parametrize("error_style", ["legacy", "lowercase-scaffolding", "uppercase-scaffolding"])
@pytest.mark.parametrize("sandbox_id", ["existing", "Mixed-Case"])
def test_absent_teardown_accepts_only_exact_missing_networks(monkeypatch, error_style, sandbox_id):
backend = _restricted_backend()
backend._pending_cleanup_ports["old-generation"] = (sandbox_id, 18080)
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
monkeypatch.setattr(backend, "_stop_container", lambda *_args: None)
commands = []
# Docker CLI v29 lowercases client-side error scaffolding; the daemon-side
# wrapper is stable, but both shapes must prove absence identically.
error_formats = {
"legacy": ("Error: No such container: {}", "Error response from daemon: network {} not found"),
"lowercase-scaffolding": ("error: no such object: {}", "error: network {} not found"),
"uppercase-scaffolding": ("ERROR: NO SUCH OBJECT: {}", "ERROR: NETWORK {} NOT FOUND"),
}
container_error, network_error = error_formats[error_style]
def runtime(cmd, **kwargs):
commands.append(cmd)
if cmd[:3] == ["docker", "rm", "-f"]:
return SimpleNamespace(stdout="", stderr=container_error.format(cmd[-1]), returncode=1)
return SimpleNamespace(stdout="", stderr=network_error.format(cmd[-1]), returncode=1)
monkeypatch.setattr("subprocess.run", runtime)
backend.complete_absent_teardown(sandbox_id)
assert released == [18080]
assert backend._pending_cleanup_ports == {}
assert ["docker", "network", "rm", backend._resource_names(sandbox_id)[1]] in commands
assert ["docker", "network", "rm", backend._egress_network_name(sandbox_id)] in commands
@pytest.mark.parametrize(
("message", "expected"),
[
("Error response from daemon: network sandbox-net not found", True),
("error response from daemon: network sandbox-net not found", True),
("error: network sandbox-net not found", True),
("Error: No such network: sandbox-net", True),
("error: no such network: sandbox-net", True),
("No such network: sandbox-net", True),
("network sandbox-net not found", True),
("error: network sandbox-net-extra not found", False),
("error: network Sandbox-Net not found", False),
("error: no such network: sandbox-other", False),
("error: network sandbox-net has active endpoints", False),
('context "sandbox-net": context not found', False),
("", False),
],
)
def test_is_no_such_network_error_matches_scaffolding_case_insensitively(message, expected):
assert local_backend_module._is_no_such_network_error(message, "sandbox-net") is expected
@pytest.mark.parametrize(
("message", "expected"),
[
# Docker <=28 client-side and daemon-side forms.
("Error: No such object: sandbox-x", True),
("Error response from daemon: No such container: sandbox-x", True),
("No such object: sandbox-x", True),
# Docker 29 lowercases the client-side scaffolding.
("error: no such object: sandbox-x", True),
("error response from daemon: no such container: sandbox-x", True),
# Scaffolding is normalized; the name stays byte-exact and the whole
# message must match.
("error: no such object: Sandbox-X", False),
("error: no such object: sandbox-x-extra", False),
("error: no such object", False),
("error: no such image: sandbox-x", False),
("prefix error: no such object: sandbox-x", False),
("error: no such object: sandbox-x suffix", False),
# Apple Container forms stay exact-match.
("Error: container not found: sandbox-x", True),
('notFound: "container not found: sandbox-x"', True),
('Error: not found: "sandbox-x"', True),
("error: container not found: sandbox-x", False),
("", False),
],
)
def test_is_no_such_container_error_matches_scaffolding_case_insensitively(message, expected):
assert local_backend_module._is_no_such_container_error(message, "sandbox-x") is expected
def test_apple_absent_teardown_releases_own_unknown_generation_port_after_timeout(monkeypatch):
backend = _backend_for_inspect_tests()
backend._runtime = "container"
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
def runtime(cmd, **kwargs):
if "stop" in cmd:
raise subprocess.TimeoutExpired(cmd, 120)
assert cmd[:2] == ["container", "inspect"]
return SimpleNamespace(stdout="[]", stderr="Error: container not found: sandbox-existing", returncode=1)
monkeypatch.setattr("subprocess.run", runtime)
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(SandboxInfo("other", "http://localhost:18081", container_name="sandbox-other"))
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(SandboxInfo("existing", "http://localhost:18080", container_name="sandbox-existing"))
assert backend.inspect_runtime("existing") is None
assert backend.is_absent("existing") is True
assert released == []
backend.complete_absent_teardown("existing")
backend.complete_absent_teardown("existing")
assert released == [18080]
backend.complete_absent_teardown("other")
assert released == [18080, 18081]
def test_apple_present_unknown_generation_does_not_inherit_pending_port(monkeypatch):
backend = _backend_for_inspect_tests()
backend._runtime = "container"
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
def runtime(cmd, **kwargs):
if "stop" in cmd:
raise subprocess.TimeoutExpired(cmd, 120)
entry = {"configuration": {"id": "sandbox-existing"}, "status": "running"}
return SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(SandboxInfo("existing", "http://localhost:18080", container_name="sandbox-existing"))
info = backend.inspect_runtime("existing")
assert info is not None
assert info.container_id is None
assert info.sandbox_url == ""
assert backend.is_absent("existing") is False
assert released == []
@pytest.mark.parametrize("runtime", ["container", "docker"])
def test_successful_unknown_generation_retry_does_not_release_a_reused_port_again(monkeypatch, runtime):
from deerflow.utils.network import PortAllocator
backend = _backend_for_inspect_tests()
backend._runtime = runtime
allocator = PortAllocator()
monkeypatch.setattr(allocator, "_is_port_available", lambda port: port not in allocator._reserved_ports)
port = allocator.allocate(18080, max_range=1)
failed = False
released = []
def release(port):
released.append(port)
allocator.release(port)
def stop(_target):
nonlocal failed
if not failed:
failed = True
raise subprocess.TimeoutExpired("stop", 120)
monkeypatch.setattr(backend, "_stop_container", stop)
monkeypatch.setattr(local_backend_module, "release_port", release)
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout="", stderr="", returncode=0))
original = SandboxInfo("existing", f"http://localhost:{port}", container_name="sandbox-existing")
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(original)
# Old Docker metadata can learn its immutable generation on a retry.
if runtime == "docker":
original.container_id = "learned-generation"
backend.destroy(original)
assert allocator.allocate(18080, max_range=1) == port
backend.complete_absent_teardown("existing")
assert released == [port]
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(18080, max_range=1)
def test_absent_teardown_deduplicates_a_legacy_reservation_that_later_learns_its_generation(monkeypatch):
backend = _backend_for_inspect_tests()
released = []
monkeypatch.setattr(local_backend_module, "release_port", released.append)
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout="", stderr="", returncode=0))
def timeout(_target):
raise subprocess.TimeoutExpired("stop", 120)
monkeypatch.setattr(backend, "_stop_container", timeout)
info = SandboxInfo("existing", "http://localhost:18080", container_name="sandbox-existing")
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(info)
info.container_id = "learned-generation"
with pytest.raises(subprocess.TimeoutExpired):
backend.destroy(info)
monkeypatch.setattr(backend, "_stop_container", lambda *_args: None)
backend.complete_absent_teardown("existing")
backend.complete_absent_teardown("existing")
assert released == [18080]
@pytest.mark.parametrize("failed_stage", ["proxy-stop", "proxy-remove", "main-stop"])
def test_restricted_partial_destroy_keeps_a_retryable_main_and_releases_port_once(monkeypatch, failed_stage):
from deerflow.utils.network import PortAllocator
backend = _backend_for_inspect_tests()
backend._network_mode = "allowlist"
allocator = PortAllocator()
monkeypatch.setattr(allocator, "_is_port_available", lambda port: port not in allocator._reserved_ports)
port = allocator.allocate(18080, max_range=1)
released = []
def release(port):
released.append(port)
allocator.release(port)
monkeypatch.setattr(local_backend_module, "release_port", release)
proxy_name, _ = backend._resource_names("existing")
main = {"Id": "main-generation", "Name": "/sandbox-existing", "Config": {"Labels": {"deerflow.network_mode": "allowlist"}}}
proxy = {"Id": "proxy-generation", "Name": f"/{proxy_name}", "HostConfig": {"PortBindings": {"8080/tcp": [{"HostPort": str(port)}]}}}
removed = set()
failed = False
def runtime(cmd, **kwargs):
nonlocal failed
target = cmd[-1]
if "inspect" in cmd:
if target in removed:
return SimpleNamespace(stdout="[]", stderr=f"Error: No such object: {target}", returncode=1)
return SimpleNamespace(stdout=json.dumps([proxy if target == proxy_name else main]), stderr="", returncode=0)
stage = "proxy-remove" if cmd[:3] == ["docker", "rm", "-f"] else "proxy-stop" if "stop" in cmd and target == proxy_name else "main-stop" if "stop" in cmd else None
if stage == failed_stage and not failed:
failed = True
if kwargs.get("check"):
raise subprocess.CalledProcessError(1, cmd, stderr="daemon denied teardown")
return SimpleNamespace(stdout="", stderr="daemon denied teardown", returncode=1)
if stage == "main-stop":
removed.add("sandbox-existing")
elif stage == "proxy-remove":
removed.add(proxy_name)
return SimpleNamespace(stdout="", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
original = SandboxInfo("existing", f"http://localhost:{port}", container_name="sandbox-existing", container_id="main-generation")
with pytest.raises((RuntimeError, subprocess.CalledProcessError)):
backend.destroy(original)
assert backend.is_absent("existing") is False
assert released == []
with pytest.raises(RuntimeError, match="No available port"):
allocator.allocate(18080, max_range=1)
recovered = backend.inspect_runtime("existing")
assert recovered is not None
backend.destroy(recovered)
assert allocator.allocate(18080, max_range=1) == port
assert released == [port]
def test_apple_list_does_not_mark_unsupported_runtime_metadata_for_replacement(monkeypatch):
backend = _backend_for_inspect_tests()
backend._runtime = "container"
entry = {"configuration": {"id": "sandbox-existing"}, "status": "running"}
def runtime(cmd, **kwargs):
return SimpleNamespace(stdout="sandbox-existing\n" if "ps" in cmd else json.dumps([entry]), stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", runtime)
assert backend.list_running() == []
assert backend.inspect_runtime("existing") is not None
def test_apple_batch_preserves_legacy_docker_shaped_metadata(monkeypatch):
backend = _backend_for_inspect_tests()
backend._runtime = "container"
entry = {"Name": "/sandbox-existing", "NetworkSettings": {"Ports": {"8080/tcp": [{"HostPort": "18080"}]}}}
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout=json.dumps([entry]), stderr="", returncode=0))
assert backend._batch_inspect(["sandbox-existing"], strict=True)["sandbox-existing"].host_port == 18080
@pytest.mark.parametrize("runtime", ["docker", "container"])
def test_inspect_runtime_only_returns_none_for_authoritative_absence(monkeypatch, runtime):
backend = _backend_for_inspect_tests()
backend._runtime = runtime
error = "Error: No such object: sandbox-missing" if runtime == "docker" else "Error: container not found: sandbox-missing"
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout="[]", stderr=error, returncode=1))
assert backend.inspect_runtime("missing") is None
@pytest.mark.parametrize(
"payload",
[
"",
"[]",
"{}",
"not JSON",
json.dumps([{"Name": "/sandbox-other", "Id": "generation"}]),
json.dumps([{"Name": "/sandbox-existing"}]),
json.dumps([{"Name": "/sandbox-existing", "Id": 42}]),
json.dumps([{"Name": "/sandbox-existing", "Id": "generation"}, {"Name": "/sandbox-existing", "Id": "other-generation"}]),
],
)
def test_inspect_runtime_fails_closed_on_incomplete_or_invalid_docker_response(monkeypatch, payload):
backend = _backend_for_inspect_tests()
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout=payload, stderr="", returncode=0))
with pytest.raises(RuntimeError):
backend.inspect_runtime("existing")
with pytest.raises(RuntimeError):
backend.is_absent("existing")
@pytest.mark.parametrize("failure", [OSError("runtime unavailable"), subprocess.TimeoutExpired("inspect", 5)])
def test_inspect_runtime_propagates_runtime_failures(monkeypatch, failure):
backend = _backend_for_inspect_tests()
def failed(*_args, **_kwargs):
raise failure
monkeypatch.setattr("subprocess.run", failed)
with pytest.raises(RuntimeError):
backend.inspect_runtime("existing")
def test_batch_inspect_keeps_present_generations_with_exact_partial_absence(monkeypatch):
backend = _backend_for_inspect_tests()
entry = {"Name": "/sandbox-existing", "Id": "immutable-generation"}
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout=json.dumps([entry]), stderr="Error: No such object: sandbox-missing", returncode=1))
inspections = backend._batch_inspect(["sandbox-existing", "sandbox-missing"], strict=True)
assert list(inspections) == ["sandbox-existing"]
assert inspections["sandbox-existing"].container_id == "immutable-generation"
def test_batch_inspect_does_not_accept_absence_of_an_unrequested_resource(monkeypatch):
backend = _backend_for_inspect_tests()
monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: SimpleNamespace(stdout="[]", stderr="Error: No such object: sandbox-other", returncode=1))
with pytest.raises(RuntimeError):
backend._batch_inspect(["sandbox-existing"], strict=True)
def test_stop_container_passes_a_timeout(monkeypatch):
"""An unbounded `stop` can outlive the teardown lease that guards it.
The `del:` marker keeps a peer from re-acquiring the container during the
stop, but a lease can lapse (a store outage longer than the TTL) while a
wedged daemon leaves `docker stop` blocked forever — and the stop then lands
on a container the peer has since been handed. Bounding the call caps that
exposure independently of the ownership layer.
"""
backend = _backend_for_inspect_tests()
seen = {}
def fake_run(cmd, **kwargs):
seen.update(kwargs)
return SimpleNamespace(stdout="", stderr="", returncode=0)
monkeypatch.setattr("subprocess.run", fake_run)
backend._stop_container("sandbox-slow")
assert seen.get("timeout") == backend._STOP_TIMEOUT_SECONDS
def test_failed_stop_is_not_reported_as_destroyed(monkeypatch):
backend = _backend_for_inspect_tests()
def denied(cmd, **_kwargs):
raise subprocess.CalledProcessError(1, cmd, stderr="daemon denied stop")
monkeypatch.setattr("subprocess.run", denied)
with pytest.raises(subprocess.CalledProcessError):
backend.destroy(SandboxInfo("sandbox", "http://localhost:18080", container_id="generation"))
def test_docker_discovery_preserves_runtime_generation(monkeypatch):
import json
import requests
backend = _backend_for_inspect_tests()
payload = [
{
"Id": "docker-generation",
"Name": "/sandbox-existing",
"Config": {"Image": "sandbox:latest", "Labels": {"deerflow.role": "sandbox", "deerflow.sandbox_id": "existing", "deerflow.network_mode": "open"}},
"NetworkSettings": {"Ports": {"8080/tcp": [{"HostPort": "18080"}]}},
}
]
def runtime(cmd, **_kwargs):
return SimpleNamespace(returncode=0, stdout="true" if "-f" in cmd else json.dumps(payload), stderr="")
monkeypatch.setattr("subprocess.run", runtime)
monkeypatch.setattr(requests.Session, "get", lambda *_args, **_kwargs: SimpleNamespace(status_code=200))
info = backend.discover("existing")
assert info is not None
assert info.container_id == "docker-generation"
def test_stop_container_propagates_a_timeout_instead_of_reporting_success(monkeypatch):
"""A timed-out stop must not be swallowed like a failed one.
`CalledProcessError` means the runtime answered "I could not stop it"; a
timeout means we do not know, and the container is probably still running.
Returning normally would let `_destroy_warm_entry` report a clean stop and
drop the warm entry, leaking a running container nothing tracks.
"""
backend = _backend_for_inspect_tests()
def fake_run(cmd, **kwargs):
raise subprocess.TimeoutExpired(cmd=cmd, timeout=kwargs["timeout"])
monkeypatch.setattr("subprocess.run", fake_run)
with pytest.raises(subprocess.TimeoutExpired):
backend._stop_container("sandbox-wedged")
# ── Extended network syntax and IPv6 host normalization ──────────────────────
def test_start_container_rejects_extended_network_syntax_host(monkeypatch):
"""name=host attaches the host network namespace exactly like `host`;
the raw string must not dodge the rejection."""
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "name=host")
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_NETWORK"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_rejects_extended_network_syntax_none(monkeypatch):
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "name=none")
with pytest.raises(RuntimeError, match="loopback-only"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_rejects_extended_network_syntax_container(monkeypatch):
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "name=container:gateway")
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_NETWORK"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_passes_extended_network_syntax_for_custom_networks(monkeypatch):
"""The legit name=<custom-net> long form (and network IDs) keep working."""
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "name=deer-flow-sandbox-egress")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("--network") + 1] == "name=deer-flow-sandbox-egress"
@pytest.mark.parametrize("sandbox_host", ["fd00::1", "[fd00::1]"])
def test_discover_brackets_ipv6_sandbox_host_for_url(monkeypatch, sandbox_host):
"""Both IPv6 input forms must yield the same bracketed URL authority:
the bare form used to produce the malformed http://fd00::1:<port>."""
backend = _backend_for_inspect_tests()
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", sandbox_host)
monkeypatch.setattr(backend, "_is_container_running", lambda name: True)
monkeypatch.setattr(
backend,
"_batch_inspect",
lambda *_args, **_kwargs: {
"sandbox-sbx-ipv6": _ContainerInspection(
1.0,
18081,
{},
"sandbox:latest",
frozenset({"bridge"}),
)
},
)
seen_urls = []
def fake_ready(url, timeout):
seen_urls.append(url)
return True
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.wait_for_sandbox_ready", fake_ready)
info = backend.discover("sbx-ipv6")
assert info.sandbox_url == "http://[fd00::1]:18081"
assert seen_urls == ["http://[fd00::1]:18081"]
@pytest.mark.parametrize("sandbox_host", ["fd00::1", "[fd00::1]"])
def test_create_brackets_ipv6_sandbox_host_for_url(monkeypatch, sandbox_host):
backend = _backend_for_inspect_tests()
monkeypatch.setenv("DEER_FLOW_SANDBOX_HOST", sandbox_host)
monkeypatch.setattr(
backend,
"_start_container",
lambda name, port, mounts=None, **_kwargs: "container-id",
)
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.get_free_port", lambda start_port=None: 18082)
info = backend.create(thread_id="t", sandbox_id="sbx-ipv6")
assert info.sandbox_url == "http://[fd00::1]:18082"
def test_resolve_sandbox_host_address_accepts_bracketed_ipv6(monkeypatch):
"""The bracketed form must resolve (unbracketed for getaddrinfo) instead
of failing through to the IPv4 bridge fallback."""
from deerflow.community.aio_sandbox.local_backend import _resolve_sandbox_host_address
infos = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", ("fd00::1", 0, 0, 0))]
def fake_getaddrinfo(host, port, *args, **kwargs):
assert host == "fd00::1", f"getaddrinfo must receive the unbracketed form, got {host!r}"
return infos
monkeypatch.setattr("deerflow.community.aio_sandbox.local_backend.socket.getaddrinfo", fake_getaddrinfo)
assert _resolve_sandbox_host_address("[fd00::1]") == "[fd00::1]"
# ── Long-syntax fields in any position (Docker opts/network.go semantics) ────
@pytest.mark.parametrize(
"network",
[
"name=host,gw-priority=0",
"gw-priority=0,name=host",
"name=host,alias=sbx",
],
)
def test_start_container_rejects_host_with_additional_long_syntax_fields(monkeypatch, network):
"""Docker accepts comma-separated fields in any order; both orderings
select the host network and must not dodge the rejection."""
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", network)
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_NETWORK"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_rejects_none_with_additional_long_syntax_fields(monkeypatch):
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "gw-priority=0,name=none")
with pytest.raises(RuntimeError, match="loopback-only"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_rejects_container_mode_with_additional_long_syntax_fields(monkeypatch):
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "name=container:gateway,gw-priority=0")
with pytest.raises(RuntimeError, match="DEER_FLOW_SANDBOX_NETWORK"):
_capture_start_container_command(monkeypatch, backend)
def test_start_container_passes_long_syntax_custom_network_with_fields(monkeypatch):
"""A legit long-syntax value with extra fields keeps passing through verbatim."""
backend = _backend_for_inspect_tests()
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_NETWORK", "name=egressnet,gw-priority=1")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert captured_cmd[captured_cmd.index("--network") + 1] == "name=egressnet,gw-priority=1"
def test_effective_network_target_last_name_field_wins():
"""Docker's parser lets a later name= field overwrite an earlier one."""
from deerflow.community.aio_sandbox.local_backend import _effective_docker_network_target as target
assert target("name=host,name=egressnet") == "egressnet"
assert target("name=egressnet,name=host") == "host"
assert target("gw-priority=0") == "gw-priority=0" # no name= field: Docker errors itself
assert target("bridge") == "bridge"
assert target("1f2a" * 16) == "1f2a" * 16 # network ID passes through
# ── Real-image startup smoke tests (docker-gated) ────────────────────────────
# Keep the baseline default in sync with aio_sandbox_provider.DEFAULT_IMAGE.
_DEFAULT_AIO_IMAGE = "enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest"
# Dedicated regression target for #5161. The workflow resolves this tag to an
# immutable digest before pytest runs so the CI result records the exact image.
_FOWNER_REGRESSION_AIO_IMAGE = "enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:1.11.0"
def _docker_daemon_available() -> bool:
try:
subprocess.run(["docker", "info"], capture_output=True, timeout=30, check=True)
return True
except Exception:
return False
def _assert_image_starts_under_hardened_capabilities(
monkeypatch,
*,
image: str,
sandbox_id: str,
failure_label: str,
) -> None:
from deerflow.community.aio_sandbox.backend import SANDBOX_LOCAL_PROVIDER_READY_TIMEOUT
from deerflow.community.aio_sandbox.local_backend import wait_for_sandbox_ready
if not _docker_daemon_available():
pytest.skip("requires a running Docker daemon")
backend = LocalContainerBackend(
image=image,
base_port=18210,
container_prefix="sandbox-smoke",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
info = backend.create(thread_id="smoke", sandbox_id=sandbox_id)
try:
# The production deadline, single-sourced: the sync and async
# provider paths destroy the container after exactly this budget, so
# a longer one here could pass while every real acquisition fails.
# (create() completes docker run — including the image pull — before
# this timer starts, so the pull is not part of the budget.)
ready = wait_for_sandbox_ready(info.sandbox_url, timeout=SANDBOX_LOCAL_PROVIDER_READY_TIMEOUT)
if not ready:
# Fail diagnosably: the entrypoint's own log tells us whether the
# capability set is still incomplete (chown/chmod/useradd/su
# errors) or the services are merely slow.
logs = subprocess.run(
["docker", "logs", info.container_name],
capture_output=True,
text=True,
timeout=30,
)
# supervisord only reports exit codes in the container log; the
# failing program's own stderr goes to files inside the container.
# Pull the usual suspects so the failure is actionable in CI.
prog_logs = subprocess.run(
[
"docker",
"exec",
info.container_name,
"sh",
"-c",
"cat /var/log/supervisor/* 2>/dev/null | tail -n 40; echo '--- nginx -t ---'; nginx -t 2>&1; echo '--- nginx error.log ---'; tail -n 20 /var/log/nginx/error.log 2>/dev/null",
],
capture_output=True,
text=True,
timeout=30,
)
tail = "\n".join((logs.stdout + logs.stderr).splitlines()[-40:]) + "\n" + (prog_logs.stdout or "")
pytest.fail(f"{failure_label} never became ready under the hardened capabilities: {info.sandbox_url}\n--- last 40 container log lines ---\n{tail}")
assert backend.is_alive(info)
finally:
backend.destroy(info)
# `live`: pulls and runs external images, so the default offline suite
# (`make test` = `-m "not live"`) never touches the network. The daemon probe
# happens inside the helper — never at collection time.
@pytest.mark.live
def test_default_image_starts_under_hardened_capabilities(monkeypatch):
"""Preserve real-image coverage for the configured/default AIO image.
This stays separate from the 1.11.0 regression because older images may
not execute the FOWNER-gated chmod added by the newer startup path.
"""
_assert_image_starts_under_hardened_capabilities(
monkeypatch,
image=os.environ.get("DEER_FLOW_SANDBOX_SMOKE_IMAGE", _DEFAULT_AIO_IMAGE),
sandbox_id="caps-smoke-default",
failure_label="configured/default image",
)
@pytest.mark.live
def test_aio_1_11_image_starts_with_fowner_capability(monkeypatch):
"""Regression smoke for #5161 against the recommended AIO 1.11.0 image.
Its entrypoint chmods /run/user/1000 after capabilities are dropped.
Without FOWNER that startup path exits before readiness; reaching the
endpoint proves the five-capability compatibility set covers the bug.
"""
_assert_image_starts_under_hardened_capabilities(
monkeypatch,
image=os.environ.get("DEER_FLOW_SANDBOX_FOWNER_SMOKE_IMAGE", _FOWNER_REGRESSION_AIO_IMAGE),
sandbox_id="caps-smoke-fowner-1-11",
failure_label="AIO 1.11.0 FOWNER regression image",
)
@pytest.mark.live
def test_restricted_network_proxy_enforces_and_approves_real_traffic(monkeypatch):
"""Exercise the Engine-28 bridge, API relay, proxy, event, and grant path."""
if not _docker_daemon_available():
pytest.skip("requires a running Docker daemon")
image = os.environ.get("DEER_FLOW_SANDBOX_NETWORK_SMOKE_IMAGE", "python:3.12-alpine")
backend = LocalContainerBackend(
image=image,
base_port=18310,
container_prefix="sandbox-policy-smoke",
config_mounts=[],
environment={},
network_config={
"mode": "allowlist",
"allow_domains": ["pypi.org"],
"approval": "prompt",
"temporary_grant_ttl": 300,
"proxy_image": image,
},
)
monkeypatch.delenv("DEER_FLOW_SANDBOX_BIND_HOST", raising=False)
sandbox_id = "network-live"
container_name = f"sandbox-policy-smoke-{sandbox_id}"
proxy_name, network_name = backend._resource_names(sandbox_id)
egress_network_name = backend._egress_network_name(sandbox_id)
port = get_free_port(start_port=18310)
relay_token = "live-relay-token-that-is-at-least-32-bytes"
proxy_url = f"http://{proxy_name}:3128"
try:
assert backend._restricted_resources_status(sandbox_id) == "missing"
backend._create_internal_network(network_name, sandbox_id)
backend._create_egress_network(egress_network_name, sandbox_id)
backend._start_network_proxy(proxy_name, network_name, egress_network_name, container_name, port, sandbox_id, relay_token)
proxy_inspect = subprocess.run(
["docker", "inspect", proxy_name],
capture_output=True,
text=True,
timeout=15,
)
assert proxy_inspect.returncode == 0, proxy_inspect.stderr
proxy_networks = json.loads(proxy_inspect.stdout)[0]["NetworkSettings"]["Networks"]
assert set(proxy_networks) == {network_name, egress_network_name}
assert "bridge" not in proxy_networks
proxy_egress_ip = proxy_networks[egress_network_name]["IPAddress"]
outside = subprocess.run(
[
"docker",
"run",
"--rm",
"--network",
"bridge",
image,
"python",
"-c",
(f"import socket,sys\ntry:\n socket.create_connection(({proxy_egress_ip!r}, 8080), timeout=2)\nexcept OSError:\n sys.exit(0)\nsys.exit(42)"),
],
capture_output=True,
text=True,
timeout=15,
)
assert outside.returncode == 0, outside.stderr or "a container on Docker's shared bridge reached the sandbox API relay"
sandbox_labels = backend._restricted_labels(sandbox_id, "sandbox")
sandbox = subprocess.run(
[
"docker",
"run",
"--rm",
"-d",
"--network",
network_name,
"--name",
container_name,
*(item for key, value in sandbox_labels.items() for item in ("--label", f"{key}={value}")),
"-e",
f"HTTP_PROXY={proxy_url}",
"-e",
f"HTTPS_PROXY={proxy_url}",
"-e",
f"http_proxy={proxy_url}",
"-e",
f"https_proxy={proxy_url}",
image,
"python",
"-m",
"http.server",
"8080",
],
capture_output=True,
text=True,
timeout=60,
)
assert sandbox.returncode == 0, sandbox.stderr
assert backend._restricted_resources_status(sandbox_id) == "compatible"
backend._network_config["allow_domains"] = ["changed.example"]
assert backend._restricted_resources_status(sandbox_id) == "mismatch"
backend._network_config["allow_domains"] = ["pypi.org"]
sandbox_url = f"http://127.0.0.1:{port}"
unauthenticated = subprocess.run(
["curl", "--fail", "--silent", "--max-time", "2", sandbox_url],
capture_output=True,
text=True,
)
assert unauthenticated.returncode != 0
deadline = time.time() + 20
while time.time() < deadline:
relay = subprocess.run(
[
"curl",
"--fail",
"--silent",
"--max-time",
"2",
"-H",
f"X-DeerFlow-Relay-Token: {relay_token}",
sandbox_url,
],
capture_output=True,
text=True,
)
if relay.returncode == 0:
break
time.sleep(0.25)
assert relay.returncode == 0, relay.stderr
def sandbox_fetch(url: str, *, use_proxy: bool = True) -> subprocess.CompletedProcess[str]:
proxy_handler = "urllib.request.ProxyHandler()" if use_proxy else "urllib.request.ProxyHandler({})"
code = f"import urllib.request; opener=urllib.request.build_opener({proxy_handler}); print(opener.open({url!r}, timeout=15).status)"
return subprocess.run(
["docker", "exec", container_name, "python", "-c", code],
capture_output=True,
text=True,
timeout=25,
)
allowed = sandbox_fetch("https://pypi.org/simple/")
assert allowed.returncode == 0, allowed.stderr
assert allowed.stdout.strip() == "200"
denied = sandbox_fetch("https://example.com/")
assert denied.returncode != 0
events = backend.consume_network_policy_events(sandbox_id)
assert [(event["host"], event["port"]) for event in events] == [("example.com", 443)]
request_id = str(events[0]["request_id"])
assert backend.decide_network_policy_request(sandbox_id, request_id, "allow_temporary")
approved = sandbox_fetch("https://example.com/")
assert approved.returncode == 0, approved.stderr
assert approved.stdout.strip() == "200"
metadata = sandbox_fetch("http://169.254.169.254/latest/meta-data/")
assert metadata.returncode != 0
assert backend.consume_network_policy_events(sandbox_id) == []
direct = sandbox_fetch("https://example.com/", use_proxy=False)
assert direct.returncode != 0
finally:
backend._cleanup_restricted_resources(sandbox_id)
release_port(port)
def test_start_container_preinitialized_image_can_drop_startup_caps(monkeypatch):
"""A custom, pre-initialized non-root image never runs the root handoff,
so CHOWN/FOWNER/SETUID/SETGID/DAC_OVERRIDE must not stay available for
the container's lifetime (chown/chmod on bind mounts, UID/GID
impersonation). Opting out with DEER_FLOW_SANDBOX_IMAGE_STARTUP_CAPS=0
drops every capability."""
backend = LocalContainerBackend(
image="my-preinitialized-sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
_clear_hardening_env(monkeypatch)
monkeypatch.setenv("DEER_FLOW_SANDBOX_IMAGE_STARTUP_CAPS", "0")
captured_cmd = _capture_start_container_command(monkeypatch, backend)
assert "--cap-drop=ALL" in captured_cmd
assert not [arg for arg in captured_cmd if arg.startswith("--cap-add=")]
security_opts = [captured_cmd[i + 1] for i, arg in enumerate(captured_cmd) if arg == "--security-opt"]
assert "no-new-privileges" in security_opts
def test_docker_subprocess_calls_pin_utf8_decoding(monkeypatch):
"""Locale-default text decoding silently loses docker CLI output on hosts
whose ANSI code page is not UTF-8 (e.g. cp936 Chinese Windows): the decode
error surfaces inside subprocess's reader thread, so ``stdout``/``stderr``
come back ``None``, and JSON-parsing call sites either crash on ``None``
or silently mis-parse. Every text-mode call in this module must therefore
pin ``encoding="utf-8"`` with ``errors="replace"``.
"""
seen: list[dict] = []
def fake_run(cmd, **kwargs):
seen.append(kwargs)
return SimpleNamespace(stdout='"Debian GNU/Linux 15"\n', stderr="", returncode=0)
_docker_server_is_desktop.cache_clear()
monkeypatch.setattr("subprocess.run", fake_run)
try:
backend = LocalContainerBackend(
image="sandbox:latest",
base_port=8080,
container_prefix="sandbox",
config_mounts=[],
environment={},
)
assert backend._docker_server_is_desktop() is False
finally:
_docker_server_is_desktop.cache_clear()
assert seen, "expected the docker detection path to shell out"
for kwargs in seen:
assert kwargs.get("encoding") == "utf-8", kwargs
assert kwargs.get("errors") == "replace", kwargs
def test_every_text_mode_subprocess_call_pins_utf8():
"""Check each text-mode subprocess call's encoding and error handling.
A text-mode call without ``encoding=`` decodes with the platform's
preferred encoding, which loses output entirely on non-UTF-8 locales.
``errors="replace"`` also keeps malformed bytes from discarding a stream.
"""
source = Path(local_backend_module.__file__).read_text(encoding="utf-8")
calls = [node for node in ast.walk(ast.parse(source)) if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "subprocess" and node.func.attr == "run"]
assert calls, "expected subprocess.run calls in the local backend"
for call in calls:
keywords = {keyword.arg: keyword.value for keyword in call.keywords}
text_mode = keywords.get("text")
if not isinstance(text_mode, ast.Constant) or text_mode.value is not True:
continue
for name, expected in (("encoding", "utf-8"), ("errors", "replace")):
value = keywords.get(name)
assert isinstance(value, ast.Constant) and value.value == expected, f"line {call.lineno}: text-mode subprocess.run must pass {name}={expected!r}"