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
agent_env/__init__.py ADDED
File without changes
@@ -0,0 +1,23 @@
1
+ """A2A Agent module for AgentEnv."""
2
+ from .a2a_agent import A2AAgent, DeployedA2AAgent, NegotiatedAgentConfig
3
+ from .store import (
4
+ A2AAgentInstanceStore, A2AAgentQuery, A2AAgentStore,
5
+ get_a2a_agent_instance_store, get_a2a_agent_store,
6
+ reset_a2a_agent_instance_store, reset_a2a_agent_store,
7
+ set_a2a_agent_instance_store, set_a2a_agent_store,
8
+ )
9
+
10
+ __all__ = [
11
+ "A2AAgent",
12
+ "DeployedA2AAgent",
13
+ "NegotiatedAgentConfig",
14
+ "A2AAgentStore",
15
+ "A2AAgentQuery",
16
+ "A2AAgentInstanceStore",
17
+ "get_a2a_agent_store",
18
+ "set_a2a_agent_store",
19
+ "reset_a2a_agent_store",
20
+ "get_a2a_agent_instance_store",
21
+ "set_a2a_agent_instance_store",
22
+ "reset_a2a_agent_instance_store",
23
+ ]
@@ -0,0 +1,600 @@
1
+ """A2A Agent model for AgentEnv."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import logging
7
+ import posixpath
8
+ import shlex
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional
11
+
12
+ from agentenv_protocol.a2a_agent import STANDARD_EXTENSIONS
13
+
14
+ from agent_env.config import get_config
15
+ from agent_env.entity_refs import EntityRef
16
+ from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy, NetworkPolicyUnsupportedError
17
+ from agent_env.artifact.artifacts.docker_image import DockerImageArtifact
18
+ from agent_env.a2a_agent.object_transfer import (
19
+ TRANSFER_TIMEOUT_SECONDS,
20
+ invoke_transfer,
21
+ skill_add_call,
22
+ )
23
+ from agent_env.providers.sandbox_providers.sandbox_provider import all_sandbox_container_env, all_sandbox_url_rewrites
24
+ from agent_env.utils.paths import validate_relative_filename
25
+ from agent_env.attribution import Attribution
26
+
27
+ if TYPE_CHECKING:
28
+ from agent_env.a2a_agent.store import A2AAgentQuery
29
+ from agent_env.bundle.authoring import AuthoringContext
30
+ from agent_env.artifact import CliArtifact
31
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
32
+ from agent_env.providers.sandbox_providers.sandbox import Sandbox
33
+ from agent_env.task_step.task_steps.add_skills import Skill
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ DEFAULT_A2A_PORT = 8000
38
+ AGENT_CARD_TIMEOUT = 300
39
+
40
+ # Rootless (not the VM socket), so a container escape is an unprivileged VM user,
41
+ # not VM root. Digest-pinned because the sidecar runs --privileged.
42
+ _DIND_IMAGE = (
43
+ "public.ecr.aws/docker/library/docker:27-dind-rootless"
44
+ "@sha256:e2ac8e8f66ae21a060b0a8e3005c70f6ed9441aabf409434463d1f6eecd38026"
45
+ )
46
+ _DIND_CONTAINER = "agent-dind"
47
+ _DIND_NETWORK = "agent-docker-net"
48
+ _DIND_PORT = 2375
49
+
50
+
51
+ @dataclass
52
+ class DeployedA2AAgent:
53
+ agent_id: str
54
+ agent_version: int
55
+ a2a_url: str
56
+ sandbox_id: str
57
+ agent_card: dict
58
+ sandbox_type: str | None = None
59
+ instance_id: str | None = None
60
+ created_at_utc: str | None = None
61
+ expires_at_utc: str | None = None
62
+ network_policy: dict | None = None
63
+
64
+ @classmethod
65
+ def from_dict(cls, data: dict) -> "DeployedA2AAgent":
66
+ return cls(
67
+ agent_id=data["agent_id"],
68
+ agent_version=data["agent_version"],
69
+ a2a_url=data["a2a_url"],
70
+ sandbox_id=data["sandbox_id"],
71
+ agent_card=data.get("agent_card") or {},
72
+ sandbox_type=data.get("sandbox_type"),
73
+ instance_id=data.get("instance_id"),
74
+ created_at_utc=data.get("created_at_utc"),
75
+ expires_at_utc=data.get("expires_at_utc"),
76
+ network_policy=data.get("network_policy"),
77
+ )
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class NegotiatedAgentConfig:
82
+ endpoint: str
83
+ fields: dict
84
+
85
+
86
+ class A2AAgent:
87
+ type: ClassVar[str] = "a2a_agent"
88
+ toml_refs: ClassVar[tuple[EntityRef, ...]] = (EntityRef.artifact("image", artifact_type="docker_image"),)
89
+ toml_keys: ClassVar[dict[str, type]] = {"image": object, "default_env_vars": dict, "metadata": dict}
90
+ # What an agent.toml's [metadata] may set: the types each key takes, and how a problem names them.
91
+ toml_metadata: ClassVar[dict[str, tuple[tuple[type, ...], str]]] = {
92
+ "default_model": ((str,), "a string"), "min_disk_size_gb": ((int, float), "a number"),
93
+ }
94
+
95
+ EXT_MCP_CONFIG: ClassVar[str] = "urn:agentenv:mcp-config/v1"
96
+ EXT_SKILL_CONFIG: ClassVar[str] = "urn:agentenv:skill-config/v1"
97
+ EXT_TRAJECTORY: ClassVar[str] = "urn:agentenv:trajectory/v1"
98
+ EXT_AGENT_CONFIG: ClassVar[str] = "urn:agentenv:agent-config/v1"
99
+ EXT_SNAPSHOT: ClassVar[str] = "urn:agentenv:snapshot/v1"
100
+ EXT_PEER_AGENTS: ClassVar[str] = "urn:agentenv:peer-agents/v1"
101
+ EXT_INSTALL: ClassVar[str] = "urn:agentenv:install/v1"
102
+ EXT_TOOLS: ClassVar[str] = "urn:agentenv:tools/v1"
103
+ EXT_TRIGGERS: ClassVar[str] = "urn:agentenv:triggers/v1"
104
+
105
+ # Whole-writable-layer changelog (capture + reconstruct) is exposed as two
106
+ # methods on the snapshot extension, so capability is per-method via
107
+ # extension_method() rather than a separate extension.
108
+ SNAPSHOT_METHOD_ENABLE_CHANGELOG: ClassVar[str] = "enable-changelog"
109
+ SNAPSHOT_METHOD_APPLY_CHANGELOG: ClassVar[str] = "apply-changelog"
110
+
111
+ @staticmethod
112
+ def find_extension(card: dict, uri: str) -> dict | None:
113
+ caps = card.get("capabilities") or {}
114
+ for ext in caps.get("extensions", []):
115
+ if ext.get("uri") == uri:
116
+ return ext
117
+ return None
118
+
119
+ @staticmethod
120
+ def extension_method(card: dict, uri: str, method: str) -> dict | None:
121
+ """Return an advertised extension method, or ``None`` if absent."""
122
+ return A2AAgent.operation(A2AAgent.find_extension(card, uri), method)[0]
123
+
124
+ @staticmethod
125
+ def operation(extension: dict | None, name: str) -> tuple[dict | None, str | None]:
126
+ """An advertised extension's ``name`` method (None when unlisted) and its endpoint path.
127
+
128
+ As the protocol renders cards: a method's own endpoint wins; otherwise one the protocol
129
+ serves at the extension's path uses the card's extension endpoint, and one it serves
130
+ elsewhere uses the protocol's path for it."""
131
+ if extension is None:
132
+ return None, None
133
+ config = extension.get("config") or extension.get("params") or {}
134
+ method = (config.get("methods") or {}).get(name)
135
+ if method and method.get("endpoint"):
136
+ return method, method["endpoint"]
137
+ definition = STANDARD_EXTENSIONS.get(extension.get("uri"))
138
+ spec = next(
139
+ (op for op in (definition.operations.values() if definition else ()) if op.name == name),
140
+ None,
141
+ )
142
+ if spec is not None and spec.path != definition.endpoint:
143
+ return method, spec.path
144
+ return method, config.get("endpoint") or (spec.path if spec else None)
145
+
146
+ @staticmethod
147
+ def negotiate_agent_config(card: dict, desired: dict) -> Optional[NegotiatedAgentConfig]:
148
+ """Filter ``desired`` to the fields the card's agent-config extension accepts (capability negotiation)."""
149
+ ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
150
+ if not ext:
151
+ return None
152
+ params = ext.get("params") or {}
153
+ supported = params.get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
154
+ fields = {k: v for k, v in desired.items() if k in supported}
155
+ if not fields:
156
+ return None
157
+ return NegotiatedAgentConfig(params.get("endpoint", "/ext/agent-config"), fields)
158
+
159
+ @staticmethod
160
+ async def add_skill(deployed: "DeployedA2AAgent", skill: "Skill") -> dict:
161
+ """Validate ``skill`` and register it on the deployed agent."""
162
+ A2AAgent._skill_extension(deployed)
163
+ await asyncio.to_thread(skill.validate)
164
+ if skill.skill_artifact_id is not None:
165
+ from agent_env.artifact import SkillArtifact
166
+ artifact = SkillArtifact.get(skill.skill_artifact_id, skill.skill_artifact_version)
167
+ return await A2AAgent.register_skill(
168
+ deployed,
169
+ name=artifact.skill_name,
170
+ description=artifact.description,
171
+ object_url=artifact.skill_object_url,
172
+ )
173
+ if skill.s3_url is not None:
174
+ return await A2AAgent.register_skill(
175
+ deployed, name=skill.name, description=skill.description, object_url=skill.s3_url
176
+ )
177
+ return await A2AAgent.register_skill(
178
+ deployed, name=skill.name, description=skill.description, skill_md=skill.to_skill_md()
179
+ )
180
+
181
+ @staticmethod
182
+ async def register_skill(
183
+ deployed: "DeployedA2AAgent",
184
+ *,
185
+ name: str,
186
+ description: str,
187
+ skill_md: str | None = None,
188
+ object_url: str | None = None,
189
+ ) -> dict:
190
+ """Send one skill, as SKILL.md text or the objects under ``object_url``, in a form the
191
+ agent's card and the object store allow; returns the agent's answer."""
192
+ add_method, add_path = A2AAgent.operation(A2AAgent._skill_extension(deployed), "add")
193
+ call = await asyncio.to_thread(
194
+ skill_add_call,
195
+ add_method,
196
+ get_config().get_object_store(),
197
+ name=name,
198
+ description=description,
199
+ skill_md=skill_md,
200
+ object_url=object_url,
201
+ )
202
+ return await invoke_transfer(
203
+ deployed.a2a_url + add_path,
204
+ call,
205
+ verb="POST",
206
+ operation=f"skill add ({name})",
207
+ timeout=TRANSFER_TIMEOUT_SECONDS,
208
+ )
209
+
210
+ @staticmethod
211
+ def _skill_extension(deployed: "DeployedA2AAgent") -> dict:
212
+ skill_ext = A2AAgent.find_extension(deployed.agent_card, A2AAgent.EXT_SKILL_CONFIG)
213
+ if not skill_ext:
214
+ raise RuntimeError(
215
+ f"Deployed agent {deployed.instance_id or deployed.agent_id} does not advertise {A2AAgent.EXT_SKILL_CONFIG}"
216
+ )
217
+ return skill_ext
218
+
219
+ @staticmethod
220
+ async def install_cli(deployed: "DeployedA2AAgent", cli_artifact: "CliArtifact", gateway_url: str) -> str:
221
+ """Install a CliArtifact bundle into the agent's filesystem at /opt/cli/<command>/.
222
+ Writes a sidecar bin/.env with the gateway URL and chmod +x's the entrypoint.
223
+ Idempotent. Returns the in-process path to the executable entrypoint."""
224
+ from agent_env.providers.sandbox_providers.sandbox import VmSandbox
225
+ from agent_env.providers.sandbox_providers.sandbox_provider import build_sandbox_provider, get_agent_sandbox_provider
226
+ provider = build_sandbox_provider(deployed.sandbox_type) if deployed.sandbox_type else get_agent_sandbox_provider()
227
+ sandbox = await provider.get_sandbox(deployed.sandbox_id)
228
+ target_root = f"/opt/cli/{cli_artifact.command_name}"
229
+ entrypoint_path = f"{target_root}/{cli_artifact.entrypoint}"
230
+ env_dir = posixpath.dirname(entrypoint_path)
231
+
232
+ # write .env first — write_file_from_text creates the parent dir; write_file_from_s3 does not.
233
+ await sandbox.write_file_from_text(f"AGENT_ENV_GATEWAY_URL={gateway_url}\n", f"{env_dir}/.env")
234
+
235
+ for rel_path, file_artifact in cli_artifact.get_cli_files().get_file_artifacts().items():
236
+ await sandbox.write_file_from_s3(file_artifact.object_url, f"{target_root}/{rel_path}")
237
+
238
+ if isinstance(sandbox, VmSandbox):
239
+ await sandbox.exec_script(f"docker exec -u 0 {shlex.quote(sandbox.container_name)} chmod +x {entrypoint_path}")
240
+ else:
241
+ await sandbox.exec("chmod", "+x", entrypoint_path)
242
+
243
+ logger.info(f"Installed CliArtifact '{cli_artifact.id}' v{cli_artifact.version} at {entrypoint_path}")
244
+ return entrypoint_path
245
+
246
+ @staticmethod
247
+ async def load_file_artifact_universe(
248
+ deployed: "DeployedA2AAgent",
249
+ universe: "FileArtifactUniverse",
250
+ destination_path: str,
251
+ ) -> dict[str, str]:
252
+ """Stage every FileArtifact in `universe` into the agent sandbox under destination_path."""
253
+ from agent_env.providers.sandbox_providers.sandbox import VmSandbox
254
+ from agent_env.providers.sandbox_providers.sandbox_provider import build_sandbox_provider, get_agent_sandbox_provider
255
+
256
+ destination = destination_path.rstrip("/") or "/"
257
+ provider = build_sandbox_provider(deployed.sandbox_type) if deployed.sandbox_type else get_agent_sandbox_provider()
258
+ sandbox = await provider.get_sandbox(deployed.sandbox_id)
259
+
260
+ file_artifacts = universe.get_file_artifacts()
261
+ if not file_artifacts:
262
+ logger.warning(
263
+ f"FileArtifactUniverse '{universe.id}' v{universe.version} has no files; nothing to load into agent"
264
+ )
265
+ return {}
266
+
267
+ dirs_to_make: set[str] = {destination}
268
+ loaded: dict[str, str] = {}
269
+ for filename in file_artifacts:
270
+ validate_relative_filename(filename)
271
+ dest_path = posixpath.join(destination, filename)
272
+ parent = posixpath.dirname(dest_path)
273
+ if parent:
274
+ dirs_to_make.add(parent)
275
+ loaded[filename] = dest_path
276
+
277
+ for d in sorted(dirs_to_make):
278
+ if isinstance(sandbox, VmSandbox):
279
+ await sandbox.exec_script(f"docker exec -u 0 {shlex.quote(sandbox.container_name)} mkdir -p {shlex.quote(d)}")
280
+ else:
281
+ await sandbox.exec("mkdir", "-p", d)
282
+
283
+ total = len(file_artifacts)
284
+ logger.info(
285
+ f"Loading FileArtifactUniverse '{universe.id}' v{universe.version} "
286
+ f"({total} file(s)) into agent at {destination}"
287
+ )
288
+ # Stage files in parallel with a bounded semaphore so a universe with
289
+ # many files doesn't accumulate per-file presign + docker cp latency.
290
+ # 8 matches the default concurrency used by the FAU `get-many` CLI.
291
+ sem = asyncio.Semaphore(8)
292
+
293
+ async def _stage(filename: str, file_artifact: "Any") -> None:
294
+ dest_path = loaded[filename]
295
+ async with sem:
296
+ logger.info(f" {file_artifact.object_url} -> {dest_path}")
297
+ await sandbox.write_file_from_s3(file_artifact.object_url, dest_path)
298
+
299
+ await asyncio.gather(*[_stage(fn, fa) for fn, fa in file_artifacts.items()])
300
+
301
+ return loaded
302
+
303
+ def __init__(
304
+ self,
305
+ id: str,
306
+ version: Optional[int],
307
+ docker_image_artifact: DockerImageArtifact,
308
+ default_env_vars: dict[str, str] | None = None,
309
+ metadata: dict[str, Any] | None = None,
310
+ ):
311
+ self.id = id
312
+ self.version = version
313
+ self.docker_image_artifact = docker_image_artifact
314
+ self.default_env_vars = default_env_vars or {}
315
+ self.metadata = metadata or {}
316
+ self._sandbox: Sandbox | None = None
317
+ self._owns_sandbox: bool = True
318
+
319
+ def to_dict(self) -> dict:
320
+ return {
321
+ "id": self.id,
322
+ "type": self.type,
323
+ "version": self.version,
324
+ "docker_image_artifact": {
325
+ "id": self.docker_image_artifact.id,
326
+ "version": self.docker_image_artifact.version,
327
+ "type": self.docker_image_artifact.type,
328
+ },
329
+ "default_env_vars": self.default_env_vars,
330
+ "metadata": self.metadata,
331
+ }
332
+
333
+ @classmethod
334
+ def from_dict(cls, data: dict) -> A2AAgent:
335
+ from agent_env.artifact import Artifact
336
+ artifact_ref = data["docker_image_artifact"]
337
+ docker_image_artifact = Artifact.get(artifact_ref["id"], version=artifact_ref["version"])
338
+ metadata = data.get("metadata", {})
339
+ # Migrate top-level fields from older documents into metadata
340
+ for key in ("min_disk_size_gb", "default_model"):
341
+ if key in data and data[key] is not None and key not in metadata:
342
+ metadata[key] = data[key]
343
+ return cls(
344
+ id=data["id"], version=data.get("version"),
345
+ docker_image_artifact=docker_image_artifact,
346
+ default_env_vars=data.get("default_env_vars", {}),
347
+ metadata=metadata,
348
+ )
349
+
350
+ @classmethod
351
+ def from_toml(cls, data: dict[str, Any], ctx: AuthoringContext) -> A2AAgent:
352
+ """Write the agent authored as ``data`` (its agent.toml, with ``image`` resolved to an image
353
+ artifact's id) under ``ctx.id`` and return it. The card isn't authored: the image serves it."""
354
+ fields = cls.accept_toml(data, ctx)
355
+ return cls.put(id=ctx.id, docker_image_artifact=ctx.artifact(fields["image"], DockerImageArtifact),
356
+ default_env_vars=fields.get("default_env_vars"), metadata=fields.get("metadata"))
357
+
358
+ @classmethod
359
+ def accept_toml(cls, data: dict[str, Any], ctx: AuthoringContext) -> dict[str, Any]:
360
+ """The keys of ``data``, an agent.toml, that an agent takes, each checked the way ``from_toml``
361
+ needs it. Raises BundleError listing every problem."""
362
+ fields, problems = ctx.accepted(data, **cls.toml_keys)
363
+ values = [f"default_env_vars.{name} must be a string, not {value!r}"
364
+ for name, value in fields.get("default_env_vars", {}).items() if not isinstance(value, str)]
365
+ for name, value in fields.get("metadata", {}).items():
366
+ if name not in cls.toml_metadata:
367
+ values.append(f"unknown [metadata] key {name!r}; an agent's [metadata] takes "
368
+ f"{' and '.join(cls.toml_metadata)}")
369
+ continue
370
+ kinds, described = cls.toml_metadata[name]
371
+ if isinstance(value, bool) or not isinstance(value, kinds):
372
+ values.append(f"metadata.{name} must be {described}, not {value!r}")
373
+ problems += [f"agent.toml: {problem}" for problem in values]
374
+ if problems:
375
+ ctx.refuse(problems)
376
+ return fields
377
+
378
+ @classmethod
379
+ def get(cls, id: str, version: Optional[int] = None) -> A2AAgent:
380
+ from .store import get_a2a_agent_store
381
+ return get_a2a_agent_store().get(id, version)
382
+
383
+ @classmethod
384
+ def put(cls, **kwargs) -> A2AAgent:
385
+ from .store import get_a2a_agent_store
386
+ kwargs.setdefault("version", None)
387
+ instance = cls(**kwargs)
388
+ return get_a2a_agent_store().put_document(instance)
389
+
390
+ @classmethod
391
+ def query(cls) -> A2AAgentQuery:
392
+ from .store import A2AAgentQuery, get_a2a_agent_store
393
+ return A2AAgentQuery(get_a2a_agent_store())
394
+
395
+ VALIDATION_ENV_ID: ClassVar[str] = "multi-caf64d"
396
+
397
+ def update_metadata(self, new_metadata: dict[str, Any]) -> None:
398
+ """Replace this agent's metadata in the database using compare-and-swap."""
399
+ from .store import get_a2a_agent_store
400
+ if self.version is None:
401
+ raise ValueError("Cannot update metadata on an unsaved agent (version is None)")
402
+ self.metadata = get_a2a_agent_store().update_metadata(self.id, self.version, self.metadata, new_metadata)
403
+
404
+ async def validate(self, on_progress: Callable[[str], None] | None = None, litellm_api_key: str | None = None) -> dict:
405
+ from agent_env.a2a_agent.validator import A2AAgentValidator
406
+ return await A2AAgentValidator.validate(self, on_progress=on_progress, litellm_api_key=litellm_api_key)
407
+
408
+ async def deploy(
409
+ self,
410
+ ttl_seconds: int = 7200,
411
+ env_vars: dict[str, str] | None = None,
412
+ a2a_port: int = DEFAULT_A2A_PORT,
413
+ cpu: float = 1.0,
414
+ memory: int = 8192,
415
+ disk_size_gb: float | None = None,
416
+ sandbox_type: str | None = None,
417
+ sandbox: Sandbox | None = None,
418
+ enable_docker: bool = False,
419
+ priority: Optional[int] = None,
420
+ network_policy: NetworkPolicy | None = None,
421
+ *,
422
+ attribution: Optional[Attribution] = None,
423
+ ) -> DeployedA2AAgent:
424
+ from agent_env.providers.sandbox_providers.sandbox_provider import (
425
+ SANDBOX_MODE_VM,
426
+ build_sandbox_provider,
427
+ get_agent_sandbox_provider,
428
+ )
429
+
430
+ attribution = dict(attribution or {})
431
+ if disk_size_gb is None:
432
+ disk_size_gb = float(self.metadata.get("min_disk_size_gb") or 10)
433
+
434
+ config = get_config()
435
+ resolved_env = dict(env_vars) if env_vars else {}
436
+ if "LITELLM_API_KEY" not in resolved_env and "LITELLM_API_KEY" not in self.default_env_vars:
437
+ resolved_env["LITELLM_API_KEY"] = config.get_litellm_api_key()
438
+ if "LITELLM_BASE_URL" not in resolved_env and "LITELLM_BASE_URL" not in self.default_env_vars:
439
+ resolved_env["LITELLM_BASE_URL"] = config.get_litellm_base_url()
440
+
441
+ merged_env = await asyncio.to_thread(self._build_merged_env, resolved_env, a2a_port)
442
+ image_name = self.docker_image_artifact.image_name
443
+
444
+ try:
445
+ if sandbox is None:
446
+ provider = build_sandbox_provider(sandbox_type) if sandbox_type else get_agent_sandbox_provider()
447
+ logger.info(f"Provisioning sandbox for A2A agent '{self.id}' via {type(provider).__name__}...")
448
+ self._sandbox = await provider.create_sandbox(
449
+ image_name=image_name, port=a2a_port, env=merged_env,
450
+ cpu=cpu, memory=memory, disk_size_gb=disk_size_gb, timeout=ttl_seconds,
451
+ attribution=attribution,
452
+ priority=priority, network_policy=network_policy,
453
+ )
454
+ self._owns_sandbox = True
455
+ logger.info(f"Sandbox created: {self._sandbox.sandbox_id} (mode={self._sandbox.mode})")
456
+ else:
457
+ if network_policy is not None and network_policy.restricts_egress:
458
+ raise NetworkPolicyUnsupportedError(
459
+ f"network_policy={network_policy.mode.value!r} cannot be applied to a pre-existing "
460
+ f"sandbox {sandbox.sandbox_id!r}; deploy the agent on its own sandbox instead"
461
+ )
462
+ self._sandbox = sandbox
463
+ self._owns_sandbox = False
464
+ logger.info(f"Using pre-existing sandbox {self._sandbox.sandbox_id} (mode={self._sandbox.mode}) for A2A agent '{self.id}'")
465
+
466
+ if enable_docker and self._sandbox.mode != SANDBOX_MODE_VM:
467
+ raise ValueError(
468
+ f"enable_docker=True requires a VM sandbox, but the agent sandbox mode "
469
+ f"is {self._sandbox.mode!r} (Docker-in-Docker is VM-only)"
470
+ )
471
+
472
+ if self._sandbox.mode == SANDBOX_MODE_VM:
473
+ logger.info("Loading Docker image into VM...")
474
+ await self._sandbox.load_docker_images([self.docker_image_artifact])
475
+ logger.info("Starting A2A agent container...")
476
+ await self._run_container(image_name, a2a_port, merged_env, enable_docker=enable_docker)
477
+
478
+ a2a_url = self._sandbox.tunnel_urls.get(a2a_port)
479
+ if not a2a_url:
480
+ raise RuntimeError(f"No tunnel URL for port {a2a_port}")
481
+
482
+ logger.info("Waiting for agent card...")
483
+ agent_card = await self._wait_for_agent_card(a2a_url)
484
+
485
+ deployed = DeployedA2AAgent(
486
+ agent_id=self.id, agent_version=self.version,
487
+ a2a_url=a2a_url, sandbox_id=self._sandbox.sandbox_id,
488
+ agent_card=agent_card, sandbox_type=self._sandbox.type,
489
+ network_policy=self._sandbox.network_policy.to_dict() if self._sandbox.network_policy else None,
490
+ )
491
+ from agent_env.a2a_agent.store import get_a2a_agent_instance_store
492
+ deployed = get_a2a_agent_instance_store().create_instance(deployed, ttl_seconds)
493
+ logger.info(f"A2A agent '{self.id}' deployed at {a2a_url} (instance={deployed.instance_id})")
494
+ return deployed
495
+ except BaseException: # a cancelled deploy closes its sandbox too
496
+ await self.close()
497
+ raise
498
+
499
+ def _build_merged_env(self, resolved_env: dict[str, str], a2a_port: int) -> dict[str, str]:
500
+ merged_env = dict(self.default_env_vars)
501
+ merged_env.update(resolved_env)
502
+ merged_env["A2A_PORT"] = str(a2a_port)
503
+ shared = get_config().get_object_store().shared_credentials_env()
504
+ if shared.keys().isdisjoint(merged_env):
505
+ merged_env.update(shared)
506
+
507
+ merged_env.update(all_sandbox_container_env())
508
+ rewrites = all_sandbox_url_rewrites()
509
+ if rewrites:
510
+ merged_env["SANDBOX_URL_REWRITES"] = json.dumps(rewrites, sort_keys=True)
511
+ return merged_env
512
+
513
+ async def close(self) -> None:
514
+ if self._sandbox is not None and self._owns_sandbox:
515
+ try:
516
+ await self._sandbox.terminate()
517
+ except Exception as e:
518
+ logger.warning(f"Error terminating sandbox: {e}")
519
+ self._sandbox = None
520
+
521
+ async def _run_container(self, image_name: str, a2a_port: int, merged_env: dict[str, str], enable_docker: bool = False) -> None:
522
+ agent_env = dict(merged_env)
523
+ setup_script = ""
524
+ network_flag = ""
525
+ if enable_docker:
526
+ logger.info("enable_docker: starting rootless Docker-in-Docker sidecar for the agent (no host socket)")
527
+ setup_script = self._dind_setup_script()
528
+ network_flag = f"--network {_DIND_NETWORK} \\\n "
529
+ agent_env["DOCKER_HOST"] = f"tcp://{_DIND_CONTAINER}:{_DIND_PORT}"
530
+
531
+ env_flags = []
532
+ for key, value in agent_env.items():
533
+ escaped_value = value.replace("'", "'\\''")
534
+ env_flags.append(f"-e {key}='{escaped_value}'")
535
+ env_str = " \\\n ".join(env_flags)
536
+
537
+ run_script = f"""#!/bin/bash
538
+ set -e
539
+ {setup_script}docker run -d \\
540
+ --name {self._sandbox.container_name} \\
541
+ -p {a2a_port}:{a2a_port} \\
542
+ {network_flag}{env_str} \\
543
+ {image_name} > /dev/null
544
+ sleep 2
545
+ """
546
+ await self._sandbox.exec_script(run_script)
547
+
548
+ def _dind_setup_script(self) -> str:
549
+ # Constants become shell vars so the body stays a raw string (no f-string
550
+ # brace escaping). TLS off is safe: the tcp endpoint is only reachable on
551
+ # the VM-local bridge.
552
+ env = (
553
+ f"DIND_IMAGE={_DIND_IMAGE}\n"
554
+ f"DIND_CONTAINER={_DIND_CONTAINER}\n"
555
+ f"DIND_NETWORK={_DIND_NETWORK}\n"
556
+ f"DIND_PORT={_DIND_PORT}\n"
557
+ )
558
+ return env + r"""docker network create "$DIND_NETWORK" >/dev/null 2>&1 || true
559
+ docker rm -f "$DIND_CONTAINER" >/dev/null 2>&1 || true
560
+ docker run -d \
561
+ --name "$DIND_CONTAINER" \
562
+ --privileged \
563
+ --network "$DIND_NETWORK" \
564
+ -e DOCKER_TLS_CERTDIR="" \
565
+ "$DIND_IMAGE" > /dev/null
566
+ echo "waiting for rootless docker daemon..."
567
+ for i in $(seq 1 60); do
568
+ if docker exec "$DIND_CONTAINER" docker -H "tcp://localhost:$DIND_PORT" info >/dev/null 2>&1; then
569
+ echo "rootless docker daemon ready"
570
+ break
571
+ fi
572
+ if [ "$i" = "60" ]; then
573
+ echo "rootless docker daemon did not become ready" >&2
574
+ docker logs --tail 60 "$DIND_CONTAINER" >&2 2>&1 || true
575
+ exit 1
576
+ fi
577
+ sleep 2
578
+ done
579
+ """
580
+
581
+ async def _wait_for_agent_card(self, base_url: str) -> dict:
582
+ import httpx
583
+ card_url = f"{base_url}/.well-known/agent.json"
584
+ async with httpx.AsyncClient(timeout=2.0) as client:
585
+ for i in range(AGENT_CARD_TIMEOUT):
586
+ try:
587
+ resp = await client.get(card_url)
588
+ if resp.status_code == 200 and resp.text.strip():
589
+ card = resp.json()
590
+ logger.info(f"Agent card received: name={card.get('name')}")
591
+ return card
592
+ except Exception:
593
+ pass
594
+ if i > 0 and i % 10 == 0:
595
+ logger.info(f"Waiting for agent card... ({i}s)")
596
+ await asyncio.sleep(1)
597
+ raise RuntimeError(
598
+ f"A2A agent '{self.id}' did not serve /.well-known/agent.json "
599
+ f"within {AGENT_CARD_TIMEOUT}s"
600
+ )