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.
Files changed (312) hide show
  1. agent_env/__init__.py +0 -0
  2. agent_env/a2a_agent/__init__.py +23 -0
  3. agent_env/a2a_agent/a2a_agent.py +600 -0
  4. agent_env/a2a_agent/conversation_store.py +356 -0
  5. agent_env/a2a_agent/object_transfer.py +609 -0
  6. agent_env/a2a_agent/protocol.py +238 -0
  7. agent_env/a2a_agent/store.py +201 -0
  8. agent_env/a2a_agent/validator.py +901 -0
  9. agent_env/artifact/__init__.py +48 -0
  10. agent_env/artifact/artifact.py +74 -0
  11. agent_env/artifact/artifacts/__init__.py +0 -0
  12. agent_env/artifact/artifacts/cli.py +83 -0
  13. agent_env/artifact/artifacts/docker_image.py +457 -0
  14. agent_env/artifact/artifacts/environment.py +89 -0
  15. agent_env/artifact/artifacts/environment_universe.py +156 -0
  16. agent_env/artifact/artifacts/file.py +214 -0
  17. agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
  18. agent_env/artifact/artifacts/skill.py +206 -0
  19. agent_env/artifact/artifacts/vm_image.py +95 -0
  20. agent_env/artifact/ref.py +15 -0
  21. agent_env/artifact/registry.py +251 -0
  22. agent_env/artifact/store.py +295 -0
  23. agent_env/artifact/universe.py +38 -0
  24. agent_env/attribution.py +41 -0
  25. agent_env/bundle/__init__.py +4 -0
  26. agent_env/bundle/_fs.py +128 -0
  27. agent_env/bundle/authoring.py +239 -0
  28. agent_env/bundle/installed.py +175 -0
  29. agent_env/bundle/ledger.py +269 -0
  30. agent_env/bundle/materialize.py +204 -0
  31. agent_env/bundle/parse.py +594 -0
  32. agent_env/bundle/plan.py +414 -0
  33. agent_env/bundle/resolve.py +404 -0
  34. agent_env/bundle/run.py +361 -0
  35. agent_env/cli/__init__.py +69 -0
  36. agent_env/cli/__main__.py +10 -0
  37. agent_env/cli/_installers.py +576 -0
  38. agent_env/cli/_plugin_changes.py +947 -0
  39. agent_env/cli/a2a_agent/__init__.py +22 -0
  40. agent_env/cli/a2a_agent/add_skill.py +97 -0
  41. agent_env/cli/a2a_agent/deploy.py +60 -0
  42. agent_env/cli/a2a_agent/get.py +15 -0
  43. agent_env/cli/a2a_agent/get_instance.py +26 -0
  44. agent_env/cli/a2a_agent/put.py +78 -0
  45. agent_env/cli/a2a_agent/validate.py +56 -0
  46. agent_env/cli/artifact/__init__.py +20 -0
  47. agent_env/cli/artifact/cli.py +83 -0
  48. agent_env/cli/artifact/environment.py +44 -0
  49. agent_env/cli/artifact/environment_universe.py +163 -0
  50. agent_env/cli/artifact/file_artifact_universe.py +281 -0
  51. agent_env/cli/artifact/skill.py +128 -0
  52. agent_env/cli/banner.py +25 -0
  53. agent_env/cli/config.py +263 -0
  54. agent_env/cli/env/__init__.py +31 -0
  55. agent_env/cli/env/deploy.py +112 -0
  56. agent_env/cli/env/gateway.py +57 -0
  57. agent_env/cli/env/get_instance.py +40 -0
  58. agent_env/cli/env/mcp_server.py +410 -0
  59. agent_env/cli/env/multi.py +282 -0
  60. agent_env/cli/env/service_db.py +99 -0
  61. agent_env/cli/env/snapshot.py +35 -0
  62. agent_env/cli/env/state/__init__.py +6 -0
  63. agent_env/cli/env/state/init.py +108 -0
  64. agent_env/cli/env/state/teardown.py +44 -0
  65. agent_env/cli/env/website.py +216 -0
  66. agent_env/cli/env/website_browser.py +67 -0
  67. agent_env/cli/eval/__init__.py +16 -0
  68. agent_env/cli/eval/add_tasks.py +63 -0
  69. agent_env/cli/eval/create.py +49 -0
  70. agent_env/cli/eval/run.py +194 -0
  71. agent_env/cli/identity/__init__.py +5 -0
  72. agent_env/cli/identity/client_identity.py +37 -0
  73. agent_env/cli/plugin.py +438 -0
  74. agent_env/cli/run.py +285 -0
  75. agent_env/cli/task/__init__.py +21 -0
  76. agent_env/cli/task/create.py +109 -0
  77. agent_env/cli/task/get.py +17 -0
  78. agent_env/cli/task/get_instance.py +34 -0
  79. agent_env/cli/task/run.py +615 -0
  80. agent_env/cli/task/validate.py +31 -0
  81. agent_env/cli/up.py +123 -0
  82. agent_env/cli/utils.py +183 -0
  83. agent_env/config/__init__.py +50 -0
  84. agent_env/config/describe.py +1067 -0
  85. agent_env/config/errors.py +13 -0
  86. agent_env/config/loader.py +245 -0
  87. agent_env/config/model.py +154 -0
  88. agent_env/config/paths.py +25 -0
  89. agent_env/config/plugin_tables.py +101 -0
  90. agent_env/config/provenance.py +134 -0
  91. agent_env/config/runtime.py +1064 -0
  92. agent_env/config/snapshot.py +81 -0
  93. agent_env/entity_refs.py +186 -0
  94. agent_env/env/__init__.py +49 -0
  95. agent_env/env/env.py +433 -0
  96. agent_env/env/env_artifact_store.py +87 -0
  97. agent_env/env/envs/__init__.py +7 -0
  98. agent_env/env/envs/_deployment.py +281 -0
  99. agent_env/env/envs/gateway_server.py +32 -0
  100. agent_env/env/envs/mcp_server.py +387 -0
  101. agent_env/env/envs/multi_env.py +780 -0
  102. agent_env/env/envs/service_db/Dockerfile +20 -0
  103. agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
  104. agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
  105. agent_env/env/envs/service_db.py +92 -0
  106. agent_env/env/envs/website.py +294 -0
  107. agent_env/env/envs/website_browser/Dockerfile +40 -0
  108. agent_env/env/envs/website_browser/__init__.py +17 -0
  109. agent_env/env/envs/website_browser/entrypoint.sh +13 -0
  110. agent_env/env/gateway/Dockerfile +14 -0
  111. agent_env/env/gateway/__init__.py +37 -0
  112. agent_env/env/gateway/clock.py +184 -0
  113. agent_env/env/gateway/constants.py +190 -0
  114. agent_env/env/gateway/entrypoint.py +156 -0
  115. agent_env/env/gateway/gateway.py +1095 -0
  116. agent_env/env/gateway/get_time.py +60 -0
  117. agent_env/env/gateway/requirements.txt +4 -0
  118. agent_env/env/gateway/triggers.py +1193 -0
  119. agent_env/env/legacy_protocol.py +146 -0
  120. agent_env/env/registry.py +93 -0
  121. agent_env/env/snapshot_store.py +372 -0
  122. agent_env/env/store.py +411 -0
  123. agent_env/eval/__init__.py +14 -0
  124. agent_env/eval/eval.py +68 -0
  125. agent_env/eval/store.py +116 -0
  126. agent_env/examples/hello/README.md +9 -0
  127. agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
  128. agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
  129. agent_env/examples/hello/tasks/hello.json +10 -0
  130. agent_env/explorer/__init__.py +12 -0
  131. agent_env/explorer/app.py +277 -0
  132. agent_env/explorer/openapi_docs.py +242 -0
  133. agent_env/explorer/plugin.py +95 -0
  134. agent_env/explorer/routers/__init__.py +1 -0
  135. agent_env/explorer/routers/common.py +174 -0
  136. agent_env/explorer/routers/conversations.py +25 -0
  137. agent_env/explorer/routers/objects.py +112 -0
  138. agent_env/explorer/routers/runs.py +475 -0
  139. agent_env/plugins/__init__.py +59 -0
  140. agent_env/plugins/_cli.py +244 -0
  141. agent_env/plugins/_discovery.py +97 -0
  142. agent_env/plugins/_inventory.py +262 -0
  143. agent_env/plugins/_registration.py +268 -0
  144. agent_env/plugins/_report.py +47 -0
  145. agent_env/plugins/_requirements.py +55 -0
  146. agent_env/providers/__init__.py +37 -0
  147. agent_env/providers/env_providers/__init__.py +12 -0
  148. agent_env/providers/env_providers/constants.py +13 -0
  149. agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
  150. agent_env/providers/env_providers/env_provider.py +237 -0
  151. agent_env/providers/env_providers/env_server_provider.py +133 -0
  152. agent_env/providers/env_state/__init__.py +49 -0
  153. agent_env/providers/env_state/env_state_provider.py +554 -0
  154. agent_env/providers/env_state/local_postgres.py +390 -0
  155. agent_env/providers/env_state/store.py +112 -0
  156. agent_env/providers/sandbox_providers/__init__.py +29 -0
  157. agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
  158. agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
  159. agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
  160. agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
  161. agent_env/providers/sandbox_providers/e2b/template.py +158 -0
  162. agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
  163. agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
  164. agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
  165. agent_env/providers/sandbox_providers/sandbox.py +387 -0
  166. agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
  167. agent_env/py.typed +0 -0
  168. agent_env/runner/__init__.py +16 -0
  169. agent_env/runner/local_runner.py +190 -0
  170. agent_env/runner/runner.py +127 -0
  171. agent_env/runner/store.py +94 -0
  172. agent_env/store/__init__.py +126 -0
  173. agent_env/store/_google.py +68 -0
  174. agent_env/store/base.py +33 -0
  175. agent_env/store/document_store/__init__.py +60 -0
  176. agent_env/store/document_store/document_store.py +519 -0
  177. agent_env/store/document_store/dynamodb_document_store.py +306 -0
  178. agent_env/store/document_store/evaluation.py +199 -0
  179. agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
  180. agent_env/store/document_store/mongo_document_store.py +259 -0
  181. agent_env/store/document_store/sqlite_document_store.py +314 -0
  182. agent_env/store/ids.py +123 -0
  183. agent_env/store/image_store/__init__.py +34 -0
  184. agent_env/store/image_store/ecr_image_store.py +131 -0
  185. agent_env/store/image_store/google_credentials.py +113 -0
  186. agent_env/store/image_store/image_store.py +113 -0
  187. agent_env/store/image_store/local_registry_image_store.py +106 -0
  188. agent_env/store/image_store/oci_registry_credentials.py +196 -0
  189. agent_env/store/local_state.py +27 -0
  190. agent_env/store/object_store/__init__.py +24 -0
  191. agent_env/store/object_store/gcs_object_store.py +415 -0
  192. agent_env/store/object_store/local_object_store.py +143 -0
  193. agent_env/store/object_store/object_store.py +198 -0
  194. agent_env/store/object_store/s3_object_store.py +354 -0
  195. agent_env/store/query.py +112 -0
  196. agent_env/store/routing.py +609 -0
  197. agent_env/store/secret_store/__init__.py +19 -0
  198. agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
  199. agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
  200. agent_env/store/secret_store/local_secret_store.py +47 -0
  201. agent_env/store/secret_store/secret_store.py +19 -0
  202. agent_env/task/__init__.py +27 -0
  203. agent_env/task/interrupts.py +109 -0
  204. agent_env/task/registry.py +27 -0
  205. agent_env/task/step_journal.py +99 -0
  206. agent_env/task/store.py +940 -0
  207. agent_env/task/task.py +815 -0
  208. agent_env/task/teardown.py +142 -0
  209. agent_env/task_step/__init__.py +47 -0
  210. agent_env/task_step/context.py +195 -0
  211. agent_env/task_step/context_ops.py +224 -0
  212. agent_env/task_step/registry.py +182 -0
  213. agent_env/task_step/review_store.py +118 -0
  214. agent_env/task_step/snapshot_utils/__init__.py +2 -0
  215. agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
  216. agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
  217. agent_env/task_step/store.py +129 -0
  218. agent_env/task_step/task_step.py +173 -0
  219. agent_env/task_step/task_steps/__init__.py +27 -0
  220. agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
  221. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
  222. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
  223. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
  224. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
  225. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
  226. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
  227. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
  228. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
  229. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
  230. agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
  231. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
  232. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
  233. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
  234. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
  235. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
  236. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
  237. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
  238. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
  239. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
  240. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
  241. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
  242. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
  243. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
  244. agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
  245. agent_env/task_step/task_steps/add_skills.py +240 -0
  246. agent_env/task_step/task_steps/apply_server_config.py +288 -0
  247. agent_env/task_step/task_steps/collect_artifacts.py +940 -0
  248. agent_env/task_step/task_steps/deploy_agent.py +626 -0
  249. agent_env/task_step/task_steps/deploy_env.py +197 -0
  250. agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
  251. agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
  252. agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
  253. agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
  254. agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
  255. agent_env/task_step/task_steps/install_agent.py +376 -0
  256. agent_env/task_step/task_steps/load_artifact.py +775 -0
  257. agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
  258. agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
  259. agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
  260. agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
  261. agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
  262. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
  263. agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
  264. agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
  265. agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
  266. agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
  267. agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
  268. agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
  269. agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
  270. agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
  271. agent_env/task_step/task_steps/peer_agents.py +92 -0
  272. agent_env/task_step/task_steps/prompt_agent.py +900 -0
  273. agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
  274. agent_env/task_step/task_steps/register_env_triggers.py +97 -0
  275. agent_env/task_step/task_steps/reset_env.py +71 -0
  276. agent_env/task_step/task_steps/review.py +152 -0
  277. agent_env/task_step/task_steps/run_code.py +461 -0
  278. agent_env/task_step/task_steps/run_code_runner.py +52 -0
  279. agent_env/task_step/task_steps/run_docker_container.py +380 -0
  280. agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
  281. agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
  282. agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
  283. agent_env/task_step/task_steps/snapshot_env.py +637 -0
  284. agent_env/task_step/task_steps/sync_env_clock.py +132 -0
  285. agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
  286. agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
  287. agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
  288. agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
  289. agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
  290. agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
  291. agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
  292. agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
  293. agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
  294. agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
  295. agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
  296. agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
  297. agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
  298. agent_env/task_step/thread_work.py +61 -0
  299. agent_env/utils/__init__.py +0 -0
  300. agent_env/utils/card_naming.py +108 -0
  301. agent_env/utils/deprecation.py +109 -0
  302. agent_env/utils/docker_build.py +29 -0
  303. agent_env/utils/exec_retry.py +77 -0
  304. agent_env/utils/litellm_attribution.py +59 -0
  305. agent_env/utils/paths.py +20 -0
  306. agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
  307. agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
  308. agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
  309. agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
  310. agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
  311. agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
  312. 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
+ )