mirror of
https://github.com/langgenius/dify.git
synced 2026-08-15 04:59:46 +08:00
fix(dify-agent): align agent and shell execution limits (#40043)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
d6336c5d49
commit
4d0bc32dd3
@ -42,7 +42,8 @@ DIFY_AGENT_ENTERPRISE_SANDBOX_PROXY_TIMEOUT=60
|
||||
# E2B backend: API key, prepared shellctl template, and active Binding policy.
|
||||
DIFY_AGENT_E2B_API_KEY=
|
||||
DIFY_AGENT_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox
|
||||
# Maximum continuous active time. Binding resources pause; temporary Home initialization resources are killed.
|
||||
# Maximum continuous active time for the RuntimeLease that spans one complete Agent run.
|
||||
# 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=
|
||||
|
||||
@ -192,8 +192,10 @@ its Binding and Workspace are one Sandbox. It also rejects binding-only destroy.
|
||||
Neither path creates a fallback Workspace or switches backends.
|
||||
|
||||
`DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS` limits continuous active time for an E2B
|
||||
resource. Runtime resources pause on timeout. It is not a retention TTL and
|
||||
does not delete paused resources or immutable snapshots.
|
||||
resource to one hour. The limit covers the complete Agent run held by one
|
||||
RuntimeLease rather than an individual tool call. Runtime resources pause on
|
||||
timeout. It is not a retention TTL and does not delete paused resources or
|
||||
immutable snapshots.
|
||||
|
||||
See the [Shell layer](../../user-manual/shell-layer/index.md) for request
|
||||
composition and the [Operations Guide](../../guide/index.md) for Local and E2B
|
||||
|
||||
@ -51,7 +51,7 @@ also reads `.env` and `dify-agent/.env` when present.
|
||||
| `DIFY_AGENT_ENTERPRISE_SANDBOX_PROXY_TIMEOUT` | `60` | Enterprise shellctl-proxy timeout in seconds. |
|
||||
| `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, up to 3600 seconds. Binding resources pause on timeout. This is not a retention TTL. |
|
||||
| `DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS` | `3600` | Maximum continuous active time for the RuntimeLease spanning one complete Agent run. Binding resources pause on timeout. This 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_SANDBOX_FILE_UPLOAD_MAX_BYTES` | `52428800` | Standalone Dify Agent maximum for whole-file Workspace upload capture; 50 MiB by default. Docker Compose derives it from `PLUGIN_MAX_FILE_SIZE`. |
|
||||
@ -236,16 +236,16 @@ Run the real E2B contract with an explicit test credential and template:
|
||||
cd dify-agent
|
||||
DIFY_AGENT_TEST_E2B_API_KEY="$E2B_API_TOKEN" \
|
||||
DIFY_AGENT_TEST_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox \
|
||||
DIFY_AGENT_TEST_E2B_ACTIVE_TIMEOUT_SECONDS=900 \
|
||||
pdm run pytest --import-mode=importlib \
|
||||
tests/integration/dify_agent/runtime_backend/test_working_environment.py \
|
||||
-k e2b -q -rs
|
||||
```
|
||||
|
||||
The Local auth token is optional when shellctl has authentication disabled.
|
||||
The E2B test timeout value `900` means up to 15 minutes of continuous active
|
||||
test time; it is not a post-test retention TTL. Both contracts create unique
|
||||
resources and perform explicit cleanup in `finally` blocks.
|
||||
The E2B contract uses the one-hour `E2B_MAX_ACTIVE_TIMEOUT_SECONDS` RuntimeLease
|
||||
limit. This is continuous active test time, not a post-test retention TTL. Both
|
||||
contracts create unique resources and perform explicit cleanup in `finally`
|
||||
blocks.
|
||||
|
||||
## Scheduling and shutdown semantics
|
||||
|
||||
@ -255,6 +255,10 @@ automatic retry layer. Request-shaped runtime failures such as bad composition,
|
||||
prompt, output, or snapshot inputs are reported later as failed runs rather than
|
||||
rejected synchronously once the request DTO itself is accepted.
|
||||
|
||||
Each run explicitly limits Pydantic AI to 100 model-request steps. Tool calls do
|
||||
not have a separate count limit, but every model request used to continue the
|
||||
tool loop consumes one of those steps.
|
||||
|
||||
During FastAPI shutdown the scheduler rejects new runs, waits up to
|
||||
`DIFY_AGENT_SHUTDOWN_GRACE_SECONDS` for active tasks, then cancels remaining tasks
|
||||
and attempts to finalize them as failed. Success, failure, cancellation, and this
|
||||
|
||||
@ -48,7 +48,7 @@ class _HasErrorCode(Protocol):
|
||||
|
||||
DEFAULT_TIMEOUT_SECONDS = 30.0
|
||||
DEFAULT_TERMINATE_GRACE_SECONDS = 10.0
|
||||
_SHELL_OUTPUT_PROMPT_EDGE_BYTES = 8 * 1024
|
||||
_SHELL_OUTPUT_PROMPT_EDGE_BYTES = 4 * 1024
|
||||
_SHELLCTL_OUTPUT_LIMIT_BYTES = 2 * _SHELL_OUTPUT_PROMPT_EDGE_BYTES
|
||||
_REMOTE_COMPLETE_OUTPUT_MAX_BYTES = 1024 * 1024
|
||||
_REMOTE_COMMAND_TIMEOUT_SECONDS = 60.0
|
||||
|
||||
@ -40,6 +40,7 @@ from pydantic_ai.exceptions import ModelHTTPError
|
||||
from pydantic_ai.messages import AgentStreamEvent, PartDeltaEvent, PartStartEvent, TextPart, TextPartDelta
|
||||
from pydantic_ai.output import OutputSpec
|
||||
from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults
|
||||
from pydantic_ai.usage import UsageLimits
|
||||
|
||||
from agenton.compositor import CompositorSessionSnapshot, LayerConfigInput, LayerProviderInput
|
||||
from agenton.layers.types import PydanticAITool
|
||||
@ -79,6 +80,7 @@ from dify_agent.runtime.user_prompt_validation import EMPTY_USER_PROMPTS_ERROR,
|
||||
|
||||
|
||||
_AGENT_OUTPUT_ADAPTER = TypeAdapter(object)
|
||||
_MAX_AGENT_STEPS_PER_RUN = 100
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@ -324,6 +326,7 @@ class AgentRunRunner:
|
||||
message_history=message_history,
|
||||
deferred_tool_results=deferred_tool_results,
|
||||
event_stream_handler=handle_events,
|
||||
usage_limits=UsageLimits(request_limit=_MAX_AGENT_STEPS_PER_RUN),
|
||||
)
|
||||
complete_usage = model.accumulated_usage if isinstance(model, _HasAccumulatedUsage) else None
|
||||
usage = _serialize_agent_usage(complete_usage if complete_usage is not None else _result_usage(result))
|
||||
|
||||
@ -40,6 +40,7 @@ from dify_agent.runtime_backend.shellctl import ShellctlRuntimeLease, create_own
|
||||
if TYPE_CHECKING:
|
||||
from e2b.connection_config import ApiParams
|
||||
|
||||
# One RuntimeLease spans the complete Agent run, not one Shell tool call.
|
||||
E2B_MAX_ACTIVE_TIMEOUT_SECONDS = 60 * 60
|
||||
_SHELLCTL_READY_MAX_ATTEMPTS = 3
|
||||
_SHELLCTL_READY_RETRY_INTERVAL_SECONDS = 0.5
|
||||
|
||||
@ -23,6 +23,7 @@ from shellctl.shared.constants import (
|
||||
DEFAULT_OUTPUT_LIMIT_BYTES,
|
||||
DEFAULT_TERMINATE_GRACE_SECONDS,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS,
|
||||
)
|
||||
from shellctl.shared.schemas import (
|
||||
DeleteJobResponse,
|
||||
@ -71,7 +72,7 @@ class ShellctlClient:
|
||||
token: str | None = None,
|
||||
client: httpx.AsyncClient | None = None,
|
||||
transport: httpx.AsyncBaseTransport | None = None,
|
||||
request_timeout_grace_seconds: float = 10.0,
|
||||
request_timeout_grace_seconds: float = SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS,
|
||||
) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.output_limit = output_limit
|
||||
|
||||
@ -31,6 +31,9 @@ if TYPE_CHECKING:
|
||||
MAX_LIST_LIMIT,
|
||||
MAX_OUTPUT_LIMIT_BYTES,
|
||||
MAX_WAIT_TIMEOUT_SECONDS,
|
||||
SHELL_TOOL_HARD_TIMEOUT_SECONDS,
|
||||
SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS,
|
||||
SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS,
|
||||
SESSION_NAME_PREFIX,
|
||||
)
|
||||
from shellctl.shared.output import (
|
||||
@ -87,6 +90,9 @@ __all__ = [
|
||||
"MAX_LIST_LIMIT",
|
||||
"MAX_OUTPUT_LIMIT_BYTES",
|
||||
"MAX_WAIT_TIMEOUT_SECONDS",
|
||||
"SHELL_TOOL_HARD_TIMEOUT_SECONDS",
|
||||
"SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS",
|
||||
"SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS",
|
||||
"SESSION_NAME_PREFIX",
|
||||
"TERMINAL_JOB_STATUSES",
|
||||
"DeleteJobResponse",
|
||||
@ -137,6 +143,9 @@ _EXPORTS = {
|
||||
"MAX_LIST_LIMIT": "shellctl.shared.constants",
|
||||
"MAX_OUTPUT_LIMIT_BYTES": "shellctl.shared.constants",
|
||||
"MAX_WAIT_TIMEOUT_SECONDS": "shellctl.shared.constants",
|
||||
"SHELL_TOOL_HARD_TIMEOUT_SECONDS": "shellctl.shared.constants",
|
||||
"SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS": "shellctl.shared.constants",
|
||||
"SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS": "shellctl.shared.constants",
|
||||
"SESSION_NAME_PREFIX": "shellctl.shared.constants",
|
||||
"OutputWindow": "shellctl.shared.output",
|
||||
"read_output_window": "shellctl.shared.output",
|
||||
|
||||
@ -12,7 +12,13 @@ DEFAULT_BASE_URL = "http://127.0.0.1:8765"
|
||||
DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 8
|
||||
MAX_OUTPUT_LIMIT_BYTES = 1024 * 1024
|
||||
DEFAULT_TIMEOUT_SECONDS = 30.0
|
||||
MAX_WAIT_TIMEOUT_SECONDS = 5.0 * 60.0
|
||||
# Single source of truth for the hard business timeout exposed by Shell tools.
|
||||
SHELL_TOOL_HARD_TIMEOUT_SECONDS = 5.0 * 60.0
|
||||
# Transport grace lets the HTTP response arrive after the Shell wait budget expires.
|
||||
SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS = 10.0
|
||||
SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS = SHELL_TOOL_HARD_TIMEOUT_SECONDS + SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS
|
||||
# Backward-compatible shellctl name; the Shell tool constant above owns the value.
|
||||
MAX_WAIT_TIMEOUT_SECONDS = SHELL_TOOL_HARD_TIMEOUT_SECONDS
|
||||
DEFAULT_IDLE_FLUSH_SECONDS = 0.5
|
||||
DEFAULT_TERMINATE_GRACE_SECONDS = 5.0
|
||||
DEFAULT_TERMINAL_COLS = 120
|
||||
@ -45,5 +51,8 @@ __all__ = [
|
||||
"MAX_LIST_LIMIT",
|
||||
"MAX_OUTPUT_LIMIT_BYTES",
|
||||
"MAX_WAIT_TIMEOUT_SECONDS",
|
||||
"SHELL_TOOL_HARD_TIMEOUT_SECONDS",
|
||||
"SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS",
|
||||
"SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS",
|
||||
"SESSION_NAME_PREFIX",
|
||||
]
|
||||
|
||||
@ -15,7 +15,7 @@ from shellctl.shared.constants import (
|
||||
DEFAULT_TERMINATE_GRACE_SECONDS,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
MAX_OUTPUT_LIMIT_BYTES,
|
||||
MAX_WAIT_TIMEOUT_SECONDS,
|
||||
SHELL_TOOL_HARD_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
@ -139,7 +139,7 @@ class RunJobRequest(ShellctlModel):
|
||||
cwd: str | None = None
|
||||
env: dict[str, str] | None = None
|
||||
terminal: TerminalSize | None = None
|
||||
timeout: float = Field(default=DEFAULT_TIMEOUT_SECONDS, gt=0, le=MAX_WAIT_TIMEOUT_SECONDS)
|
||||
timeout: float = Field(default=DEFAULT_TIMEOUT_SECONDS, gt=0, le=SHELL_TOOL_HARD_TIMEOUT_SECONDS)
|
||||
output_limit: int = Field(default=DEFAULT_OUTPUT_LIMIT_BYTES, ge=1, le=MAX_OUTPUT_LIMIT_BYTES)
|
||||
idle_flush_seconds: float = Field(default=DEFAULT_IDLE_FLUSH_SECONDS, ge=0, le=30)
|
||||
|
||||
@ -171,7 +171,7 @@ class RunJobRequest(ShellctlModel):
|
||||
class WaitJobRequest(ShellctlModel):
|
||||
"""HTTP request body for `POST /v1/jobs/{job_id}/wait`."""
|
||||
|
||||
timeout: float = Field(default=DEFAULT_TIMEOUT_SECONDS, ge=0, le=MAX_WAIT_TIMEOUT_SECONDS)
|
||||
timeout: float = Field(default=DEFAULT_TIMEOUT_SECONDS, ge=0, le=SHELL_TOOL_HARD_TIMEOUT_SECONDS)
|
||||
offset: int = Field(ge=0)
|
||||
output_limit: int = Field(default=DEFAULT_OUTPUT_LIMIT_BYTES, ge=1, le=MAX_OUTPUT_LIMIT_BYTES)
|
||||
idle_flush_seconds: float = Field(default=DEFAULT_IDLE_FLUSH_SECONDS, ge=0, le=30)
|
||||
@ -181,7 +181,7 @@ class InputJobRequest(ShellctlModel):
|
||||
"""HTTP request body for `POST /v1/jobs/{job_id}/input`."""
|
||||
|
||||
text: str
|
||||
timeout: float = Field(default=DEFAULT_TIMEOUT_SECONDS, gt=0, le=MAX_WAIT_TIMEOUT_SECONDS)
|
||||
timeout: float = Field(default=DEFAULT_TIMEOUT_SECONDS, gt=0, le=SHELL_TOOL_HARD_TIMEOUT_SECONDS)
|
||||
offset: int = Field(ge=0)
|
||||
output_limit: int = Field(default=DEFAULT_OUTPUT_LIMIT_BYTES, ge=1, le=MAX_OUTPUT_LIMIT_BYTES)
|
||||
idle_flush_seconds: float = Field(default=DEFAULT_IDLE_FLUSH_SECONDS, ge=0, le=30)
|
||||
@ -190,7 +190,7 @@ class InputJobRequest(ShellctlModel):
|
||||
class TerminateJobRequest(ShellctlModel):
|
||||
"""HTTP request body for `POST /v1/jobs/{job_id}/terminate`."""
|
||||
|
||||
grace_seconds: float = Field(default=DEFAULT_TERMINATE_GRACE_SECONDS, ge=0, le=300)
|
||||
grace_seconds: float = Field(default=DEFAULT_TERMINATE_GRACE_SECONDS, ge=0, le=SHELL_TOOL_HARD_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@ -13,7 +13,12 @@ from dify_agent.runtime_backend import (
|
||||
ExecutionBindingDestroySpec,
|
||||
HomeSnapshotCreateSpec,
|
||||
)
|
||||
from dify_agent.runtime_backend.e2b import E2BExecutionBindingBackend, E2BHomeSnapshotBackend, E2BSDKControlPlane
|
||||
from dify_agent.runtime_backend.e2b import (
|
||||
E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
E2BExecutionBindingBackend,
|
||||
E2BHomeSnapshotBackend,
|
||||
E2BSDKControlPlane,
|
||||
)
|
||||
from dify_agent.runtime_backend.local import LocalExecutionBindingBackend
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
@ -106,7 +111,11 @@ async def test_e2b_binding_checkpoint_and_collection() -> None:
|
||||
marker = uuid.uuid4().hex
|
||||
control = E2BSDKControlPlane(api_key=api_key)
|
||||
snapshots = E2BHomeSnapshotBackend(control_plane=control)
|
||||
bindings = E2BExecutionBindingBackend(control_plane=control, template=template, active_timeout_seconds=3600)
|
||||
bindings = E2BExecutionBindingBackend(
|
||||
control_plane=control,
|
||||
template=template,
|
||||
active_timeout_seconds=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
checkpoint_ref: str | None = None
|
||||
allocation = None
|
||||
checkpoint_allocation = None
|
||||
|
||||
@ -588,6 +588,7 @@ def test_shell_run_formats_large_non_truncated_output_without_tail_lookup() -> N
|
||||
metadata, output = _parse_tagged_observation(result)
|
||||
assert metadata["output_path"] == "/tmp/large.log"
|
||||
assert output.startswith("head-y")
|
||||
assert "max output size is limited to 8192 bytes" in output
|
||||
assert output.endswith("(check the /tmp/large.log for full output)")
|
||||
assert "-tail" in output
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ from pydantic_ai.messages import (
|
||||
from pydantic_ai.models import ModelRequestParameters
|
||||
from pydantic_ai.models.test import TestModel
|
||||
from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults
|
||||
from pydantic_ai.usage import UsageLimits
|
||||
from pydantic_ai.settings import ModelSettings
|
||||
|
||||
from agenton.compositor import CompositorSessionSnapshot, LayerProvider, LayerSessionSnapshot
|
||||
@ -608,6 +609,36 @@ def test_runner_preserves_explicit_json_null_output(monkeypatch: pytest.MonkeyPa
|
||||
assert sink.statuses["run-null-output"] == "succeeded"
|
||||
|
||||
|
||||
def test_runner_passes_explicit_step_limit_to_agent(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient):
|
||||
assert http_client.is_closed is False
|
||||
return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType]
|
||||
|
||||
class FakeAgent:
|
||||
async def run(self, *_args: object, **kwargs: object) -> FakeAgentRunResult:
|
||||
usage_limits = cast(UsageLimits, kwargs["usage_limits"])
|
||||
assert usage_limits.request_limit == 100
|
||||
return FakeAgentRunResult("done", [])
|
||||
|
||||
monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model)
|
||||
monkeypatch.setattr("dify_agent.runtime.runner.create_agent", lambda *_args, **_kwargs: FakeAgent())
|
||||
sink = InMemoryRunEventSink()
|
||||
|
||||
async def scenario() -> None:
|
||||
async with httpx.AsyncClient() as client:
|
||||
await AgentRunRunner(
|
||||
sink=sink,
|
||||
request=_request(),
|
||||
run_id="run-explicit-step-limit",
|
||||
plugin_daemon_http_client=client,
|
||||
dify_api_http_client=client,
|
||||
).run()
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
assert sink.statuses["run-explicit-step-limit"] == "succeeded"
|
||||
|
||||
|
||||
def test_runner_emits_deferred_tool_call_and_persists_pending_history(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured_output_types: list[object] = []
|
||||
captured_user_prompts: list[object] = []
|
||||
|
||||
@ -19,6 +19,7 @@ from dify_agent.runtime_backend import (
|
||||
)
|
||||
from dify_agent.runtime_backend import e2b as e2b_module
|
||||
from dify_agent.runtime_backend.e2b import (
|
||||
E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
E2BExecutionBindingBackend,
|
||||
E2BHomeSnapshotBackend,
|
||||
E2BRuntimeLease,
|
||||
@ -133,7 +134,7 @@ def _connected_backend(*, pause_error: Exception | None = None) -> tuple[E2BExec
|
||||
E2BExecutionBindingBackend(
|
||||
control_plane=control, # pyright: ignore[reportArgumentType]
|
||||
template="prepared-template",
|
||||
active_timeout_seconds=3600,
|
||||
active_timeout_seconds=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
),
|
||||
sandbox,
|
||||
)
|
||||
@ -148,7 +149,7 @@ async def test_e2b_binding_uses_default_template_or_exact_snapshot_and_couples_r
|
||||
bindings = E2BExecutionBindingBackend(
|
||||
control_plane=control, # pyright: ignore[reportArgumentType]
|
||||
template="prepared-template",
|
||||
active_timeout_seconds=3600,
|
||||
active_timeout_seconds=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
default_allocation = await bindings.create_binding(
|
||||
@ -199,7 +200,7 @@ async def test_e2b_rejects_shared_workspace_and_binding_only_destroy() -> None:
|
||||
backend = E2BExecutionBindingBackend(
|
||||
control_plane=control, # pyright: ignore[reportArgumentType]
|
||||
template="prepared-template",
|
||||
active_timeout_seconds=3600,
|
||||
active_timeout_seconds=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
spec = ExecutionBindingCreateSpec(
|
||||
tenant_id="tenant-1",
|
||||
@ -224,7 +225,7 @@ async def test_e2b_binding_create_kills_sandbox_when_initialization_fails() -> N
|
||||
backend = E2BExecutionBindingBackend(
|
||||
control_plane=control, # pyright: ignore[reportArgumentType]
|
||||
template="prepared-template",
|
||||
active_timeout_seconds=3600,
|
||||
active_timeout_seconds=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
with pytest.raises(BindingCreateError, match="pause failed"):
|
||||
@ -255,7 +256,7 @@ async def test_e2b_missing_explicit_snapshot_does_not_fall_back_to_template() ->
|
||||
backend = E2BExecutionBindingBackend(
|
||||
control_plane=control, # pyright: ignore[reportArgumentType]
|
||||
template="prepared-template",
|
||||
active_timeout_seconds=3600,
|
||||
active_timeout_seconds=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
with pytest.raises(BindingCreateError, match="snapshot unavailable"):
|
||||
|
||||
@ -12,11 +12,12 @@ from dify_agent.runtime_backend.profile import (
|
||||
)
|
||||
|
||||
|
||||
def test_e2b_backend_uses_prepared_dify_template_by_default() -> None:
|
||||
def test_e2b_backend_uses_prepared_dify_template_and_one_hour_lease_by_default() -> None:
|
||||
settings = RuntimeBackendSettings(runtime_backend="e2b", e2b_api_key="secret")
|
||||
|
||||
assert settings.e2b_template == "difys-default-team/dify-agent-local-sandbox"
|
||||
assert settings.e2b_template == DEFAULT_E2B_TEMPLATE
|
||||
assert E2B_MAX_ACTIVE_TIMEOUT_SECONDS == 60 * 60
|
||||
assert settings.e2b_active_timeout_seconds == E2B_MAX_ACTIVE_TIMEOUT_SECONDS
|
||||
|
||||
|
||||
|
||||
@ -7,13 +7,27 @@ from pydantic import ValidationError
|
||||
|
||||
from shellctl.shared import (
|
||||
JOB_ID_ALPHABET,
|
||||
MAX_WAIT_TIMEOUT_SECONDS,
|
||||
RunJobRequest,
|
||||
SHELL_TOOL_HARD_TIMEOUT_SECONDS,
|
||||
SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS,
|
||||
SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS,
|
||||
generate_job_id,
|
||||
read_output_window,
|
||||
tail_output_window,
|
||||
)
|
||||
|
||||
|
||||
def test_shell_tool_timeout_budget_has_one_source_of_truth() -> None:
|
||||
assert MAX_WAIT_TIMEOUT_SECONDS == SHELL_TOOL_HARD_TIMEOUT_SECONDS == 300
|
||||
assert SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS == 10
|
||||
assert (
|
||||
SHELL_TOOL_TIMEOUT_WITH_HTTP_GRACE_SECONDS
|
||||
== SHELL_TOOL_HARD_TIMEOUT_SECONDS + SHELL_TOOL_HTTP_TIMEOUT_GRACE_SECONDS
|
||||
== 310
|
||||
)
|
||||
|
||||
|
||||
def test_generate_job_id_matches_proposal_format() -> None:
|
||||
job_id = generate_job_id(now=datetime(2026, 5, 21, 15, 30, tzinfo=UTC))
|
||||
|
||||
|
||||
@ -280,6 +280,7 @@ DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN=
|
||||
# E2B_API_KEY and E2B_API_TOKEN remain accepted as deployment-level fallbacks.
|
||||
DIFY_AGENT_E2B_API_KEY=
|
||||
DIFY_AGENT_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox
|
||||
# One-hour RuntimeLease limit spanning a complete Agent run.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS=3600
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN=
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT=5004
|
||||
|
||||
@ -676,6 +676,7 @@ services:
|
||||
DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN: ${DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN:-${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}}
|
||||
DIFY_AGENT_E2B_API_KEY: ${DIFY_AGENT_E2B_API_KEY:-${E2B_API_KEY:-${E2B_API_TOKEN:-}}}
|
||||
DIFY_AGENT_E2B_TEMPLATE: ${DIFY_AGENT_E2B_TEMPLATE:-difys-default-team/dify-agent-local-sandbox}
|
||||
# One-hour RuntimeLease limit spanning a complete Agent run.
|
||||
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}
|
||||
|
||||
@ -682,6 +682,7 @@ services:
|
||||
DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN: ${DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN:-${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}}
|
||||
DIFY_AGENT_E2B_API_KEY: ${DIFY_AGENT_E2B_API_KEY:-${E2B_API_KEY:-${E2B_API_TOKEN:-}}}
|
||||
DIFY_AGENT_E2B_TEMPLATE: ${DIFY_AGENT_E2B_TEMPLATE:-difys-default-team/dify-agent-local-sandbox}
|
||||
# One-hour RuntimeLease limit spanning a complete Agent run.
|
||||
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}
|
||||
|
||||
@ -29,6 +29,7 @@ DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN=
|
||||
# E2B_API_KEY and E2B_API_TOKEN remain accepted as deployment-level fallbacks.
|
||||
DIFY_AGENT_E2B_API_KEY=
|
||||
DIFY_AGENT_E2B_TEMPLATE=difys-default-team/dify-agent-local-sandbox
|
||||
# One-hour RuntimeLease limit spanning a complete Agent run.
|
||||
DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS=3600
|
||||
DIFY_AGENT_E2B_SHELLCTL_AUTH_TOKEN=
|
||||
DIFY_AGENT_E2B_SHELLCTL_PORT=5004
|
||||
|
||||
Loading…
Reference in New Issue
Block a user