mirror of
https://github.com/langgenius/dify.git
synced 2026-08-30 17:11:50 +08:00
fix(dify-agent): require E2B traffic authentication (#40871)
This commit is contained in:
@@ -46,7 +46,6 @@ DIFY_AGENT_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox
|
||||
# Binding resources pause; temporary Home initialization resources are killed.
|
||||
# This is not a retention TTL for paused resources or immutable snapshots.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS=3600
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN=
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT=5004
|
||||
# JSON array of regex patterns to redact from shell output shown to the agent.
|
||||
DIFY_AGENT_SHELL_REDACT_PATTERNS=
|
||||
|
||||
@@ -54,7 +54,6 @@ also reads `.env` and `dify-agent/.env` when present.
|
||||
| `DIFY_AGENT_E2B_API_KEY` | empty | E2B API key; required for E2B. |
|
||||
| `DIFY_AGENT_E2B_TEMPLATE` | `difys-default-team/dify-agent-local-sandbox` | Prepared E2B template containing shellctl and the deployment-default Home environment. |
|
||||
| `DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS` | `3600` | Maximum continuous active time for the RuntimeLease spanning one complete Agent run. Its default intentionally matches `DIFY_AGENT_RUN_TIMEOUT_SECONDS`, but the settings are independently configurable. Binding resources pause on timeout; this setting does not own the run terminal state and is not a retention TTL. |
|
||||
| `DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN` | empty | Optional bearer token expected by shellctl inside the E2B template. |
|
||||
| `DIFY_AGENT_E2B_SHELLCTL_PORT` | `5004` | shellctl port exposed by the E2B template. |
|
||||
| `DIFY_AGENT_SHELL_REDACT_PATTERNS` | empty | JSON array of additional regex patterns redacted from Shell output. |
|
||||
| `DIFY_AGENT_STUB_API_BASE_URL` | empty | HTTP(S) Agent Stub API base URL reachable from the Sandbox. It may be the service root or `/agent-stub`. Enables `DIFY_AGENT_STUB_*` env injection for user `shell.run` jobs. |
|
||||
|
||||
@@ -77,8 +77,11 @@ DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN=replace-with-shellctl-token
|
||||
# DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT=/tmp/dify-agent/home-snapshots
|
||||
```
|
||||
|
||||
The auth token may be empty when shellctl authentication is disabled. E2B uses
|
||||
`DIFY_AGENT_E2B_API_KEY`, the prepared template, and its shellctl settings.
|
||||
The auth token may be empty when shellctl authentication is disabled. Dify-created
|
||||
E2B Sandboxes disable public traffic at creation and access shellctl through the
|
||||
E2B port proxy with its `traffic_access_token`. Acquiring a RuntimeLease fails if
|
||||
E2B does not provide a non-empty token. This policy applies only to newly created
|
||||
Sandboxes and does not retrofit existing ones.
|
||||
|
||||
To let shell jobs call the Agent Stub with `dify-agent ...`, configure a
|
||||
Sandbox-reachable Agent Stub URL and a unique production secret. Remote
|
||||
|
||||
@@ -126,6 +126,7 @@ class E2BSDKControlPlane:
|
||||
template,
|
||||
timeout=timeout,
|
||||
metadata=metadata,
|
||||
network={"allow_public_traffic": False},
|
||||
lifecycle={"on_timeout": on_timeout, "auto_resume": False},
|
||||
**self._options(),
|
||||
),
|
||||
@@ -209,7 +210,6 @@ class E2BExecutionBindingBackend:
|
||||
control_plane: E2BControlPlane
|
||||
template: str
|
||||
active_timeout_seconds: int
|
||||
shellctl_auth_token: str = ""
|
||||
shellctl_port: int = 5004
|
||||
layout: RuntimeLayout = field(
|
||||
default_factory=lambda: RuntimeLayout(home_dir="/home/dify", workspace_dir="/home/dify/workspace")
|
||||
@@ -308,14 +308,17 @@ class E2BExecutionBindingBackend:
|
||||
async def _lease(self, sandbox: _E2BSandbox) -> "E2BRuntimeLease":
|
||||
entrypoint = f"https://{sandbox.get_host(self.shellctl_port)}"
|
||||
traffic_token = sandbox.traffic_access_token
|
||||
headers = {"X-Access-Token": traffic_token} if isinstance(traffic_token, str) and traffic_token else {}
|
||||
if not isinstance(traffic_token, str) or not traffic_token:
|
||||
raise BindingAcquireError("E2B sandbox did not provide a traffic access token")
|
||||
http_client = httpx.AsyncClient(
|
||||
base_url=entrypoint,
|
||||
headers=headers,
|
||||
headers={"X-Access-Token": traffic_token},
|
||||
follow_redirects=True,
|
||||
timeout=httpx.Timeout(60.0),
|
||||
)
|
||||
|
||||
# Explicit token="" prevents process-level SHELLCTL_AUTH_TOKEN fallback;
|
||||
# E2B port access is authenticated only by X-Access-Token above.
|
||||
def client_factory() -> ShellctlClientProtocol:
|
||||
from shellctl.client import ShellctlClient
|
||||
|
||||
@@ -323,7 +326,7 @@ class E2BExecutionBindingBackend:
|
||||
ShellctlClientProtocol,
|
||||
cast(
|
||||
object,
|
||||
ShellctlClient(entrypoint, token=self.shellctl_auth_token, client=http_client),
|
||||
ShellctlClient(entrypoint, token="", client=http_client),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -331,7 +334,7 @@ class E2BExecutionBindingBackend:
|
||||
handle=sandbox.sandbox_id,
|
||||
layout=self.layout,
|
||||
entrypoint=entrypoint,
|
||||
token=self.shellctl_auth_token,
|
||||
token="",
|
||||
client_factory=client_factory,
|
||||
owned_transport=http_client,
|
||||
)
|
||||
|
||||
@@ -60,7 +60,6 @@ class RuntimeBackendSettings(BaseSettings):
|
||||
ge=1,
|
||||
le=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
e2b_shellctl_auth_token: str = ""
|
||||
e2b_shellctl_port: int = Field(default=5004, ge=1, le=65535)
|
||||
|
||||
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
|
||||
@@ -146,7 +145,6 @@ def create_runtime_backend_profile(settings: RuntimeBackendSettings) -> RuntimeB
|
||||
control_plane=control_plane,
|
||||
template=settings.e2b_template,
|
||||
active_timeout_seconds=settings.e2b_active_timeout_seconds,
|
||||
shellctl_auth_token=settings.e2b_shellctl_auth_token,
|
||||
shellctl_port=settings.e2b_shellctl_port,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -71,7 +71,6 @@ class ServerSettings(BaseSettings):
|
||||
ge=1,
|
||||
le=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
e2b_shellctl_auth_token: str = ""
|
||||
e2b_shellctl_port: int = Field(default=5004, ge=1, le=65535)
|
||||
agent_stub_api_base_url: str | None = Field(default=None, validation_alias="DIFY_AGENT_STUB_API_BASE_URL")
|
||||
sandbox_files_base_url: str | None = Field(
|
||||
@@ -205,7 +204,6 @@ class ServerSettings(BaseSettings):
|
||||
e2b_api_key=self.e2b_api_key,
|
||||
e2b_template=self.e2b_template,
|
||||
e2b_active_timeout_seconds=self.e2b_active_timeout_seconds,
|
||||
e2b_shellctl_auth_token=self.e2b_shellctl_auth_token,
|
||||
e2b_shellctl_port=self.e2b_shellctl_port,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from dify_agent.runtime_backend.e2b import (
|
||||
E2BExecutionBindingBackend,
|
||||
E2BHomeSnapshotBackend,
|
||||
E2BRuntimeLease,
|
||||
E2BSDKControlPlane,
|
||||
)
|
||||
from dify_agent.runtime_backend.shellctl import ShellctlRuntimeLease
|
||||
|
||||
@@ -140,6 +141,36 @@ def _connected_backend(*, pause_error: Exception | None = None) -> tuple[E2BExec
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_e2b_sdk_create_disables_public_traffic(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from e2b import AsyncSandbox
|
||||
|
||||
sandbox = _Sandbox(sandbox_id="sandbox-1")
|
||||
create_options: dict[str, object] = {}
|
||||
|
||||
async def create(
|
||||
_cls: type[AsyncSandbox],
|
||||
template: str,
|
||||
**options: object,
|
||||
) -> _Sandbox:
|
||||
assert template == "prepared-template"
|
||||
create_options.update(options)
|
||||
return sandbox
|
||||
|
||||
monkeypatch.setattr(AsyncSandbox, "create", classmethod(create))
|
||||
control_plane = E2BSDKControlPlane(api_key="e2b-secret")
|
||||
|
||||
created = await control_plane.create(
|
||||
"prepared-template",
|
||||
timeout=120,
|
||||
metadata={"dify.resource": "runtime-sandbox"},
|
||||
on_timeout="pause",
|
||||
)
|
||||
|
||||
assert created is sandbox
|
||||
assert create_options["network"] == {"allow_public_traffic": False}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_e2b_binding_uses_default_template_or_exact_snapshot_and_couples_refs() -> None:
|
||||
control = _ControlPlane()
|
||||
@@ -299,11 +330,14 @@ async def test_e2b_checkpoint_uses_exact_source_runtime() -> None:
|
||||
async def test_e2b_acquire_retries_transient_shellctl_failures_until_ready(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("SHELLCTL_AUTH_TOKEN", "ambient-shellctl-token")
|
||||
attempts = 0
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
assert request.headers["X-Access-Token"] == "traffic-token"
|
||||
assert "Authorization" not in request.headers
|
||||
if attempts == 1:
|
||||
raise httpx.ReadTimeout("shellctl starting", request=request)
|
||||
if attempts == 2:
|
||||
@@ -328,6 +362,26 @@ async def test_e2b_acquire_retries_transient_shellctl_failures_until_ready(
|
||||
assert clients[0].is_closed
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("traffic_access_token", [None, ""])
|
||||
async def test_e2b_acquire_fails_closed_without_traffic_token(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
traffic_access_token: str | None,
|
||||
) -> None:
|
||||
def handler(_request: httpx.Request) -> httpx.Response:
|
||||
raise AssertionError("shellctl must not be called without an E2B traffic access token")
|
||||
|
||||
clients = _mock_http(monkeypatch, handler)
|
||||
backend, sandbox = _connected_backend()
|
||||
sandbox.traffic_access_token = traffic_access_token
|
||||
|
||||
with pytest.raises(BindingAcquireError, match="traffic access token"):
|
||||
_ = await backend.acquire(sandbox.sandbox_id)
|
||||
|
||||
assert clients == []
|
||||
assert sandbox.pauses == [True]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_e2b_acquire_closes_transport_and_pauses_after_readiness_retries_exhausted(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
@@ -283,7 +283,6 @@ DIFY_AGENT_E2B_API_KEY=
|
||||
DIFY_AGENT_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox
|
||||
# RuntimeLease active limit; its default matches the independently configurable Agent run deadline.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS=3600
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN=
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT=5004
|
||||
# Sandbox-reachable Dify API base for dify-agent CLI /files/* transfers.
|
||||
# Remote Sandboxes should use the public Dify ingress; local Compose uses api via agent_ssrf_proxy.
|
||||
|
||||
@@ -677,7 +677,6 @@ services:
|
||||
DIFY_AGENT_E2B_TEMPLATE: ${DIFY_AGENT_E2B_TEMPLATE:-difys-default-team/dify-agent-local-sandbox}
|
||||
# RuntimeLease active limit; its default matches the independently configurable Agent run deadline.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS: ${DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS:-3600}
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN:-}
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT: ${DIFY_AGENT_E2B_SHELLCTL_PORT:-5004}
|
||||
DIFY_AGENT_STUB_API_BASE_URL: ${DIFY_AGENT_STUB_API_BASE_URL:-http://agent_backend:5050/agent-stub}
|
||||
DIFY_AGENT_SANDBOX_FILES_BASE_URL: ${DIFY_AGENT_SANDBOX_FILES_BASE_URL:-http://api:5001}
|
||||
|
||||
@@ -683,7 +683,6 @@ services:
|
||||
DIFY_AGENT_E2B_TEMPLATE: ${DIFY_AGENT_E2B_TEMPLATE:-difys-default-team/dify-agent-local-sandbox}
|
||||
# RuntimeLease active limit; its default matches the independently configurable Agent run deadline.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS: ${DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS:-3600}
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN:-}
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT: ${DIFY_AGENT_E2B_SHELLCTL_PORT:-5004}
|
||||
DIFY_AGENT_STUB_API_BASE_URL: ${DIFY_AGENT_STUB_API_BASE_URL:-http://agent_backend:5050/agent-stub}
|
||||
DIFY_AGENT_SANDBOX_FILES_BASE_URL: ${DIFY_AGENT_SANDBOX_FILES_BASE_URL:-http://api:5001}
|
||||
|
||||
@@ -32,7 +32,6 @@ DIFY_AGENT_E2B_API_KEY=
|
||||
DIFY_AGENT_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox
|
||||
# RuntimeLease active limit; its default matches the independently configurable Agent run deadline.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS=3600
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN=
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT=5004
|
||||
# Sandbox-reachable Dify API base for signed /files/* transfers.
|
||||
DIFY_AGENT_SANDBOX_FILES_BASE_URL=http://api:5001
|
||||
|
||||
Reference in New Issue
Block a user