agentenv-framework 0.9.1254__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""E2B's async sandbox presented through the :class:`VmSandbox` contract.
|
|
2
|
+
|
|
3
|
+
The E2B SDK intentionally returns a completed ``CommandResult`` from
|
|
4
|
+
``commands.run``. ``VmSandbox``, on the other hand, is built on a small
|
|
5
|
+
process interface whose stdout and stderr are async byte streams. This module
|
|
6
|
+
bridges those two shapes without initializing E2B SDK clients at module import
|
|
7
|
+
time, keeping provider registration and offline unit tests isolated.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
import shlex
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
from agent_env.providers.sandbox_providers.sandbox import NetworkMode, NetworkPolicy, VmSandbox
|
|
21
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_VM
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from e2b import AsyncSandbox
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
E2B_ALL_TRAFFIC = "0.0.0.0/0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _BytesReader:
|
|
32
|
+
"""The small ``StreamReader`` subset consumed by ``Sandbox.exec_with_output``."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, value: str | bytes | None):
|
|
35
|
+
self._value = value.encode() if isinstance(value, str) else (value or b"")
|
|
36
|
+
|
|
37
|
+
async def read(self) -> bytes:
|
|
38
|
+
return self._value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _E2BCompletedProcess:
|
|
42
|
+
"""Adapt E2B's completed command result to the common process protocol."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, result: Any):
|
|
45
|
+
self.stdout = _BytesReader(getattr(result, "stdout", ""))
|
|
46
|
+
self.stderr = _BytesReader(getattr(result, "stderr", ""))
|
|
47
|
+
self._exit_code = int(getattr(result, "exit_code", -1))
|
|
48
|
+
|
|
49
|
+
async def wait(self) -> int:
|
|
50
|
+
return self._exit_code
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _E2BTunnelURLs(dict[int, str]):
|
|
54
|
+
"""A port map that asks E2B for an external host on first use.
|
|
55
|
+
|
|
56
|
+
E2B doesn't allocate persistent tunnel objects. Its ``get_host(port)``
|
|
57
|
+
derives the public endpoint for any port, so laziness both avoids a stale
|
|
58
|
+
static port list after reconnect and preserves the normal ``tunnel_urls``
|
|
59
|
+
indexing used throughout agent-env.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, sandbox: E2BSandbox):
|
|
63
|
+
super().__init__()
|
|
64
|
+
self._sandbox = sandbox
|
|
65
|
+
|
|
66
|
+
def __missing__(self, port: int) -> str:
|
|
67
|
+
return self._sandbox.get_host(port)
|
|
68
|
+
|
|
69
|
+
def get(self, port: int, default: str | None = None) -> str | None:
|
|
70
|
+
# ``dict.get`` is used as a capability query by deployment callers.
|
|
71
|
+
# Unlike indexing, it must not imply that every arbitrary port has a
|
|
72
|
+
# reachable E2B endpoint (nor should it mutate this cache).
|
|
73
|
+
return super().get(port, default)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class E2BSandbox(VmSandbox):
|
|
77
|
+
"""An E2B Linux sandbox with Docker, exposed as an agent-env VM sandbox.
|
|
78
|
+
|
|
79
|
+
``sandbox`` is an already-created ``e2b.AsyncSandbox``. Keeping creation
|
|
80
|
+
in the provider lets this adapter stay focused on VM process, lifecycle,
|
|
81
|
+
reconnect, and public-port behavior.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
type = "e2b"
|
|
85
|
+
# ``docker info`` should normally return in well under one second. Keep
|
|
86
|
+
# each readiness probe short so a wedged daemon cannot consume the entire
|
|
87
|
+
# VM-ready budget in one await.
|
|
88
|
+
_DOCKER_READINESS_COMMAND_TIMEOUT = 10
|
|
89
|
+
_MIN_COMMAND_TIMEOUT_SECONDS = 0.001
|
|
90
|
+
_UNBOUNDED_COMMAND_TIMEOUT_SECONDS = 0
|
|
91
|
+
_DOCKER_LOG_TAIL_LINES = 40
|
|
92
|
+
_UNKNOWN_EXIT_CODE = -1
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
sandbox: AsyncSandbox | Any,
|
|
97
|
+
*,
|
|
98
|
+
exposed_ports: Iterable[int] = (),
|
|
99
|
+
network_policy: NetworkPolicy | None = None,
|
|
100
|
+
):
|
|
101
|
+
self._sandbox = sandbox
|
|
102
|
+
self.sandbox_id = sandbox.sandbox_id
|
|
103
|
+
self.vnc_url = None
|
|
104
|
+
self.mode = SANDBOX_MODE_VM
|
|
105
|
+
self.network_policy = network_policy
|
|
106
|
+
self.tunnel_urls = _E2BTunnelURLs(self)
|
|
107
|
+
for port in exposed_ports:
|
|
108
|
+
self.get_host(port)
|
|
109
|
+
|
|
110
|
+
async def terminate(self) -> None:
|
|
111
|
+
"""Irreversibly stop the E2B sandbox (``kill`` is idempotent)."""
|
|
112
|
+
killed = await self._sandbox.kill()
|
|
113
|
+
if not killed:
|
|
114
|
+
logger.info("E2B sandbox %s was already absent during terminate", self.sandbox_id)
|
|
115
|
+
|
|
116
|
+
async def exec(self, *command: str) -> _E2BCompletedProcess:
|
|
117
|
+
"""Run argv safely through E2B's shell-command API.
|
|
118
|
+
|
|
119
|
+
E2B accepts one command string and itself invokes Bash. ``shlex.join``
|
|
120
|
+
keeps arguments distinct, notably the scripts supplied to inherited
|
|
121
|
+
``exec_script``. Do not strip ``sudo``: E2B commands run as the normal
|
|
122
|
+
sandbox user and Docker administration requires the inherited sudo
|
|
123
|
+
prefixes (``sudo bash`` / ``sudo docker``).
|
|
124
|
+
"""
|
|
125
|
+
return await self._run_command(
|
|
126
|
+
*command,
|
|
127
|
+
timeout=self._UNBOUNDED_COMMAND_TIMEOUT_SECONDS,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def _run_command(
|
|
131
|
+
self,
|
|
132
|
+
*command: str,
|
|
133
|
+
timeout: float,
|
|
134
|
+
) -> _E2BCompletedProcess:
|
|
135
|
+
"""Run an E2B command with an explicit SDK command timeout.
|
|
136
|
+
|
|
137
|
+
Normal VM operations remain unbounded (``timeout=0``) because image
|
|
138
|
+
loads and agent commands can legitimately run for a long time.
|
|
139
|
+
Readiness uses this primitive with a finite timeout so its wall-clock
|
|
140
|
+
budget remains meaningful even when Docker is wedged.
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
result = await self._sandbox.commands.run(shlex.join(command), timeout=timeout)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
# E2B raises CommandExitException for a non-zero foreground command;
|
|
146
|
+
# it is also a CommandResult, so expose that failure through the
|
|
147
|
+
# common process result rather than treating it as an SDK transport
|
|
148
|
+
# error. Other exceptions (network/auth/etc.) must still surface.
|
|
149
|
+
if not all(hasattr(exc, attribute) for attribute in ("exit_code", "stdout", "stderr")):
|
|
150
|
+
raise
|
|
151
|
+
result = exc
|
|
152
|
+
return _E2BCompletedProcess(result)
|
|
153
|
+
|
|
154
|
+
async def _exec_with_output_bounded(
|
|
155
|
+
self,
|
|
156
|
+
*command: str,
|
|
157
|
+
timeout: float,
|
|
158
|
+
) -> tuple[int, str, str]:
|
|
159
|
+
"""Execute one short readiness/diagnostic command with an E2B timeout."""
|
|
160
|
+
process = await self._run_command(*command, timeout=timeout)
|
|
161
|
+
stdout, stderr = await asyncio.gather(process.stdout.read(), process.stderr.read())
|
|
162
|
+
return await process.wait(), stdout.decode(), stderr.decode()
|
|
163
|
+
|
|
164
|
+
async def setup_vm_for_gateway(self, exposed_ports: list[int] | None = None) -> None:
|
|
165
|
+
"""Start Docker and verify the VM has the Compose v2 gateway runtime.
|
|
166
|
+
|
|
167
|
+
E2B publishes every sandbox port through its dynamic ``get_host``
|
|
168
|
+
endpoint, so unlike the KubeVirt implementation there is no guest
|
|
169
|
+
firewall to configure. ``EnvironmentGatewayProvider`` starts all co-located
|
|
170
|
+
services with ``docker compose up``, so accepting a Docker-only base
|
|
171
|
+
template here would defer a deterministic configuration error until
|
|
172
|
+
deploy-env is already staging images and files.
|
|
173
|
+
"""
|
|
174
|
+
await self.wait_for_vm()
|
|
175
|
+
exit_code, version, stderr = await self._exec_with_output_bounded(
|
|
176
|
+
"sudo",
|
|
177
|
+
"docker",
|
|
178
|
+
"compose",
|
|
179
|
+
"version",
|
|
180
|
+
timeout=self._DOCKER_READINESS_COMMAND_TIMEOUT,
|
|
181
|
+
)
|
|
182
|
+
if exit_code != 0:
|
|
183
|
+
detail = (stderr or version).strip()
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"E2B base template must include the Docker Compose v2 plugin "
|
|
186
|
+
"required by deploy-env"
|
|
187
|
+
+ (f": {detail}" if detail else "")
|
|
188
|
+
)
|
|
189
|
+
logger.info(
|
|
190
|
+
"Docker Compose ready in E2B sandbox %s: %s",
|
|
191
|
+
self.sandbox_id,
|
|
192
|
+
version.strip(),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def wait_for_vm(self) -> None:
|
|
196
|
+
"""Ensure E2B's Docker daemon is available to inherited VM helpers."""
|
|
197
|
+
await self._ensure_docker_running()
|
|
198
|
+
|
|
199
|
+
async def _ensure_docker_running(self) -> None:
|
|
200
|
+
# The base template normally has systemd, but retain service and direct
|
|
201
|
+
# dockerd fallbacks for custom templates. `nohup ... &` detaches the
|
|
202
|
+
# last fallback from E2B's completed-command stream.
|
|
203
|
+
deadline = time.monotonic() + self._VM_READY_TIMEOUT
|
|
204
|
+
start_command = (
|
|
205
|
+
"sudo systemctl start docker || sudo service docker start || "
|
|
206
|
+
"(sudo nohup dockerd > /var/log/dockerd.log 2>&1 &)"
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
start_timeout = max(
|
|
210
|
+
self._MIN_COMMAND_TIMEOUT_SECONDS,
|
|
211
|
+
min(self._DOCKER_READINESS_COMMAND_TIMEOUT, self._VM_READY_TIMEOUT),
|
|
212
|
+
)
|
|
213
|
+
start_result = await self._run_command("bash", "-c", start_command, timeout=start_timeout)
|
|
214
|
+
if await start_result.wait() != 0:
|
|
215
|
+
logger.debug("E2B Docker startup command exited nonzero in %s", self.sandbox_id)
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
logger.debug("E2B Docker startup command did not complete: %s", exc)
|
|
218
|
+
|
|
219
|
+
attempts = 0
|
|
220
|
+
while time.monotonic() < deadline:
|
|
221
|
+
attempts += 1
|
|
222
|
+
remaining = deadline - time.monotonic()
|
|
223
|
+
command_timeout = min(self._DOCKER_READINESS_COMMAND_TIMEOUT, remaining)
|
|
224
|
+
try:
|
|
225
|
+
exit_code, _, _ = await self._exec_with_output_bounded(
|
|
226
|
+
"sudo",
|
|
227
|
+
"docker",
|
|
228
|
+
"info",
|
|
229
|
+
timeout=command_timeout,
|
|
230
|
+
)
|
|
231
|
+
except Exception as exc:
|
|
232
|
+
# A timed-out or temporarily unavailable command is simply a
|
|
233
|
+
# failed readiness probe. The enclosing deadline, rather than
|
|
234
|
+
# a single SDK call, owns the readiness budget.
|
|
235
|
+
logger.debug(
|
|
236
|
+
"Docker readiness probe failed in E2B sandbox %s: %s",
|
|
237
|
+
self.sandbox_id,
|
|
238
|
+
exc,
|
|
239
|
+
)
|
|
240
|
+
exit_code = self._UNKNOWN_EXIT_CODE
|
|
241
|
+
if exit_code == 0:
|
|
242
|
+
logger.info("Docker ready in E2B sandbox %s after %s polls", self.sandbox_id, attempts)
|
|
243
|
+
return
|
|
244
|
+
sleep_seconds = min(self._VM_READY_POLL_INTERVAL, max(0, deadline - time.monotonic()))
|
|
245
|
+
if sleep_seconds:
|
|
246
|
+
await asyncio.sleep(sleep_seconds)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
_, log_tail, _ = await self._exec_with_output_bounded(
|
|
250
|
+
"sudo",
|
|
251
|
+
"tail",
|
|
252
|
+
"-n",
|
|
253
|
+
str(self._DOCKER_LOG_TAIL_LINES),
|
|
254
|
+
"/var/log/dockerd.log",
|
|
255
|
+
timeout=self._DOCKER_READINESS_COMMAND_TIMEOUT,
|
|
256
|
+
)
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
log_tail = f"unavailable ({type(exc).__name__}: {exc})"
|
|
259
|
+
raise RuntimeError(
|
|
260
|
+
f"Docker not ready in E2B sandbox {self.sandbox_id} after "
|
|
261
|
+
f"{self._VM_READY_TIMEOUT}s ({attempts} polls); "
|
|
262
|
+
f"/var/log/dockerd.log tail:\n{log_tail}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def get_host(self, port: int) -> str:
|
|
266
|
+
"""Return and cache E2B's current HTTPS URL for ``port``.
|
|
267
|
+
|
|
268
|
+
``AsyncSandbox.get_host`` returns a hostname, not a URL. Normalizing it
|
|
269
|
+
here keeps ``tunnel_urls`` compatible with the other providers.
|
|
270
|
+
"""
|
|
271
|
+
normalized_port = int(port)
|
|
272
|
+
host = self._sandbox.get_host(normalized_port)
|
|
273
|
+
url = host if host.startswith(("http://", "https://")) else f"https://{host}"
|
|
274
|
+
self.tunnel_urls[normalized_port] = url
|
|
275
|
+
return url
|
|
276
|
+
|
|
277
|
+
async def apply_network_policy(self, policy: NetworkPolicy) -> None:
|
|
278
|
+
"""Atomically replace E2B egress policy with ``policy``.
|
|
279
|
+
|
|
280
|
+
E2B's ``update_network`` clears omitted fields. ALLOW_ALL explicitly
|
|
281
|
+
enables internet access. ALLOWLIST combines E2B's canonical
|
|
282
|
+
deny-all sentinel with explicit hostname/CIDR exceptions.
|
|
283
|
+
"""
|
|
284
|
+
# ``allow_public_traffic`` is creation-only and is silently discarded
|
|
285
|
+
# by update_network. Do not confuse ingress configuration with the
|
|
286
|
+
# workload's outbound policy here.
|
|
287
|
+
network: dict[str, Any] = {"allow_internet_access": True}
|
|
288
|
+
if policy.mode is NetworkMode.ALLOWLIST:
|
|
289
|
+
network["allow_out"] = [*policy.allow_hosts, *policy.allow_cidrs]
|
|
290
|
+
network["deny_out"] = [E2B_ALL_TRAFFIC]
|
|
291
|
+
await self._sandbox.update_network(network)
|
|
292
|
+
self.network_policy = policy
|
|
293
|
+
|
|
294
|
+
async def load_docker_images(self, artifacts: list) -> None:
|
|
295
|
+
"""Load images after adding their signed-download hosts to the policy.
|
|
296
|
+
|
|
297
|
+
With a restrictive policy, the object store that supplies image tarballs
|
|
298
|
+
is infrastructure rather than a workload destination. E2B replaces an
|
|
299
|
+
egress policy atomically, so union the exact signed-URL hostnames into the
|
|
300
|
+
sandbox's applied policy. CIDRs stay untouched through
|
|
301
|
+
:meth:`NetworkPolicy.with_hosts`.
|
|
302
|
+
"""
|
|
303
|
+
policy = self.network_policy
|
|
304
|
+
if not artifacts:
|
|
305
|
+
return
|
|
306
|
+
if policy is None:
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
f"Cannot load Docker images in reconnected E2B sandbox {self.sandbox_id}: "
|
|
309
|
+
"its applied network policy is unknown, so signed download hosts cannot "
|
|
310
|
+
"be added safely"
|
|
311
|
+
)
|
|
312
|
+
if not policy.restricts_egress:
|
|
313
|
+
await super().load_docker_images(artifacts)
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
signed_urls = await self._signed_image_urls(artifacts)
|
|
317
|
+
signed_hosts = {
|
|
318
|
+
parsed.hostname
|
|
319
|
+
for signed_url in signed_urls
|
|
320
|
+
if signed_url and (parsed := urlparse(signed_url)).hostname
|
|
321
|
+
}
|
|
322
|
+
if signed_hosts:
|
|
323
|
+
await self.apply_network_policy(policy.with_hosts(sorted(signed_hosts)))
|
|
324
|
+
await self._load_docker_images(artifacts, signed_urls)
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
async def reconnect(
|
|
328
|
+
cls,
|
|
329
|
+
sandbox_id: str,
|
|
330
|
+
*,
|
|
331
|
+
api_key: str | None = None,
|
|
332
|
+
sandbox_cls: type[AsyncSandbox] | Any | None = None,
|
|
333
|
+
exposed_ports: Iterable[int] = (),
|
|
334
|
+
network_policy: NetworkPolicy | None = None,
|
|
335
|
+
) -> E2BSandbox:
|
|
336
|
+
"""Reconnect to an existing sandbox without relying on ambient credentials."""
|
|
337
|
+
if sandbox_cls is None:
|
|
338
|
+
# Optional dependency: importing this adapter must not require E2B.
|
|
339
|
+
from e2b import AsyncSandbox as sandbox_cls
|
|
340
|
+
|
|
341
|
+
sandbox = await sandbox_cls.connect(sandbox_id, api_key=api_key)
|
|
342
|
+
return cls(sandbox, exposed_ports=exposed_ports, network_policy=network_policy)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Deterministic E2B template aliases for requested sandbox sizing.
|
|
2
|
+
|
|
3
|
+
E2B fixes a sandbox's CPU and memory at template-build time. This resolver
|
|
4
|
+
derives a template from a configured base template and gives that derivative a
|
|
5
|
+
stable alias, so equivalent requests reuse the same template.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import math
|
|
13
|
+
import threading
|
|
14
|
+
from numbers import Real
|
|
15
|
+
from typing import Any, ClassVar
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_DEFAULT_BUILD_TIMEOUT_SECONDS = 15 * 60
|
|
20
|
+
_LOCK_POLL_INTERVAL_SECONDS = 0.05
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normalize_positive_integer(value: Real, *, field: str) -> int:
|
|
24
|
+
"""Return an integral positive resource value, rejecting ambiguous sizes."""
|
|
25
|
+
if (
|
|
26
|
+
isinstance(value, bool)
|
|
27
|
+
or not isinstance(value, Real)
|
|
28
|
+
or not math.isfinite(value)
|
|
29
|
+
or value <= 0
|
|
30
|
+
or int(value) != value
|
|
31
|
+
):
|
|
32
|
+
raise ValueError(f"{field} must be a positive integer; got {value!r}")
|
|
33
|
+
return int(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_e2b_cpu(cpu: float) -> int:
|
|
37
|
+
"""Validate an exact CPU request accepted by E2B's integer API."""
|
|
38
|
+
return _normalize_positive_integer(cpu, field="cpu")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_e2b_memory_mb(memory_mb: int) -> int:
|
|
42
|
+
"""Validate an exact memory request accepted by E2B's integer API."""
|
|
43
|
+
return _normalize_positive_integer(memory_mb, field="memory_mb")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class E2BTemplateResolver:
|
|
47
|
+
"""Resolve a size-specific E2B template, building it at most once locally.
|
|
48
|
+
|
|
49
|
+
Locks are shared by all resolver instances because providers may be
|
|
50
|
+
constructed more than once in the same process. E2B itself remains the
|
|
51
|
+
authority across processes: a failed build is accepted if a follow-up
|
|
52
|
+
existence check shows another worker created the same alias.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_locks: ClassVar[dict[str, threading.Lock]] = {}
|
|
56
|
+
_locks_guard: ClassVar[threading.Lock] = threading.Lock()
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
api_key: str | None = None,
|
|
62
|
+
async_template_cls: type[Any] | None = None,
|
|
63
|
+
template_cls: type[Any] | None = None,
|
|
64
|
+
build_timeout_seconds: float = _DEFAULT_BUILD_TIMEOUT_SECONDS,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._api_key = api_key
|
|
67
|
+
self._async_template_cls = async_template_cls
|
|
68
|
+
self._template_cls = template_cls
|
|
69
|
+
if build_timeout_seconds <= 0:
|
|
70
|
+
raise ValueError("build_timeout_seconds must be positive")
|
|
71
|
+
self._build_timeout_seconds = build_timeout_seconds
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def _lock_for(cls, name: str) -> threading.Lock:
|
|
75
|
+
# A threading lock is safe when separate worker threads each run their
|
|
76
|
+
# own event loop. ``resolve`` acquires it with non-blocking polls so it
|
|
77
|
+
# neither blocks the event loop nor leaks an acquired lock if cancelled.
|
|
78
|
+
with cls._locks_guard:
|
|
79
|
+
return cls._locks.setdefault(name, threading.Lock())
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
async def _acquire_lock(lock: threading.Lock) -> None:
|
|
83
|
+
while not lock.acquire(blocking=False):
|
|
84
|
+
await asyncio.sleep(_LOCK_POLL_INTERVAL_SECONDS)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _log_build(name: str, entry: Any) -> None:
|
|
88
|
+
level = getattr(entry, "level", "info")
|
|
89
|
+
message = getattr(entry, "message", str(entry))
|
|
90
|
+
log = logger.warning if level in {"warn", "error"} else logger.info
|
|
91
|
+
log("E2B template build %s: %s", name, message)
|
|
92
|
+
|
|
93
|
+
def _sdk(self) -> tuple[type[Any], type[Any]]:
|
|
94
|
+
if self._async_template_cls is None or self._template_cls is None:
|
|
95
|
+
# Defer SDK initialization until an E2B backend is actually used.
|
|
96
|
+
from e2b import AsyncTemplate, Template
|
|
97
|
+
|
|
98
|
+
return AsyncTemplate, Template
|
|
99
|
+
return self._async_template_cls, self._template_cls
|
|
100
|
+
|
|
101
|
+
async def resolve(self, base_template: str, *, cpu: float, memory_mb: int) -> str:
|
|
102
|
+
"""Return the E2B alias for ``base_template`` at the requested size."""
|
|
103
|
+
exact_cpu = normalize_e2b_cpu(cpu)
|
|
104
|
+
exact_memory_mb = normalize_e2b_memory_mb(memory_mb)
|
|
105
|
+
name = f"{base_template}-{exact_cpu}c-{exact_memory_mb}m"
|
|
106
|
+
api_params = {"api_key": self._api_key} if self._api_key is not None else {}
|
|
107
|
+
|
|
108
|
+
async_template_cls, template_cls = self._sdk()
|
|
109
|
+
lock = self._lock_for(name)
|
|
110
|
+
await self._acquire_lock(lock)
|
|
111
|
+
try:
|
|
112
|
+
if await async_template_cls.exists(name, **api_params):
|
|
113
|
+
return name
|
|
114
|
+
|
|
115
|
+
template = template_cls().from_template(base_template)
|
|
116
|
+
logger.info(
|
|
117
|
+
"Building E2B template %s from %s (cpu=%s, memory=%sMB, timeout=%ss)",
|
|
118
|
+
name,
|
|
119
|
+
base_template,
|
|
120
|
+
exact_cpu,
|
|
121
|
+
exact_memory_mb,
|
|
122
|
+
self._build_timeout_seconds,
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
async with asyncio.timeout(self._build_timeout_seconds):
|
|
126
|
+
await async_template_cls.build(
|
|
127
|
+
template,
|
|
128
|
+
name,
|
|
129
|
+
cpu_count=exact_cpu,
|
|
130
|
+
memory_mb=exact_memory_mb,
|
|
131
|
+
on_build_logs=lambda entry: self._log_build(name, entry),
|
|
132
|
+
**api_params,
|
|
133
|
+
)
|
|
134
|
+
except Exception:
|
|
135
|
+
# A separate process may have created the deterministic alias
|
|
136
|
+
# after our initial exists check. In that case its template is
|
|
137
|
+
# exactly the resource variant we needed, so reuse it.
|
|
138
|
+
if await async_template_cls.exists(name, **api_params):
|
|
139
|
+
return name
|
|
140
|
+
raise
|
|
141
|
+
return name
|
|
142
|
+
finally:
|
|
143
|
+
lock.release()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def resolve_e2b_template(
|
|
147
|
+
base_template: str,
|
|
148
|
+
*,
|
|
149
|
+
api_key: str | None = None,
|
|
150
|
+
cpu: float,
|
|
151
|
+
memory_mb: int,
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Resolve a size-specific E2B template using the default SDK classes."""
|
|
154
|
+
return await E2BTemplateResolver(api_key=api_key).resolve(
|
|
155
|
+
base_template,
|
|
156
|
+
cpu=cpu,
|
|
157
|
+
memory_mb=memory_mb,
|
|
158
|
+
)
|