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,108 @@
1
+ """Verify core environment-protocol operation support (env mirror of VerifyCoreA2AProtocolStep)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import ClassVar, Optional
7
+
8
+ import httpx
9
+ from agentenv_protocol import METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH
10
+
11
+ from agent_env.task_step.context import TaskStepContext
12
+ from agent_env.entity_refs import EntityRef
13
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # (method, probe_params); None = no mutation-free probe — resolve from the advertised operations.
18
+ _CORE_OPERATIONS = [
19
+ (METHOD_RESET, None),
20
+ (METHOD_ADD, {"parts": []}),
21
+ (METHOD_GET, {}),
22
+ ]
23
+
24
+ VALIDATED_ENVIRONMENT_PROTOCOL_KEY = "validated_environment_protocol"
25
+
26
+
27
+ class VerifyCoreEnvironmentProtocolStep(TaskStep):
28
+ type: ClassVar[str] = "verify_env_core_protocol"
29
+ entity_refs = (EntityRef.env("env_id"),)
30
+
31
+ def __init__(
32
+ self,
33
+ id: str,
34
+ version: Optional[int],
35
+ env_id: str,
36
+ depends_on: Optional[list[TaskStepDependency]] = None,
37
+ fail_task_on_error: bool = True,
38
+ ):
39
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
40
+ self.env_id = env_id
41
+
42
+ def to_dict(self) -> dict:
43
+ base = super().to_dict()
44
+ base["env_id"] = self.env_id
45
+ return base
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict) -> VerifyCoreEnvironmentProtocolStep:
49
+ return cls(**cls._base_from_dict(data), env_id=data["env_id"])
50
+
51
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
52
+ from agentenv_protocol import client as protocol_v1
53
+ from agent_env.env.env import Env
54
+
55
+ deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
56
+ if deployed is None:
57
+ raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
58
+
59
+ card = None
60
+ try:
61
+ card = await protocol_v1.get_card(deployed.environment_url)
62
+ except Exception as e:
63
+ logger.warning(f"env card for '{self.env_id}' inaccessible at {deployed.environment_url}: {type(e).__name__}: {e}")
64
+
65
+ protocol = {}
66
+ for method, probe_params in _CORE_OPERATIONS:
67
+ if probe_params is not None:
68
+ supported = await self._probe_operation(deployed.environment_url, method, probe_params)
69
+ else:
70
+ supported = self._advertised_support(card, method)
71
+ protocol[method] = {"supported": supported}
72
+
73
+ logger.info(f"core environment protocol for '{self.env_id}': " + " ".join(f"{m}={v['supported']}" for m, v in protocol.items()))
74
+
75
+ env = Env.get(self.env_id, deployed.env_version)
76
+ env.update_metadata({**env.metadata, VALIDATED_ENVIRONMENT_PROTOCOL_KEY: protocol})
77
+ context.metadata.setdefault("verifications", {})["env_core_protocol"] = protocol
78
+ return context
79
+
80
+ @staticmethod
81
+ def _advertised_support(card: Optional[dict], method: str) -> Optional[bool]:
82
+ """Per the backing card's advertised operations; None if unresolvable. An absent
83
+ key predates advertisement, when construction required the full data trio."""
84
+ if not card:
85
+ return None
86
+ children = card.get("children_environments") or []
87
+ backing = children[0] if len(children) == 1 else (card if not children else None)
88
+ if backing is None:
89
+ return None
90
+ advertised = (backing.get("capabilities") or {}).get("operations")
91
+ return True if advertised is None else method in advertised
92
+
93
+ async def _probe_operation(self, environment_url: str, method: str, params: dict) -> Optional[bool]:
94
+ """Mutation-free: -32601 = unregistered; -32000 = gateway couldn't forward (None);
95
+ anything else — a result, or -32602 from the empty-parts add probe — proves registration."""
96
+ body = {"jsonrpc": "2.0", "id": f"verify-{method}", "method": method, "params": params}
97
+ try:
98
+ async with httpx.AsyncClient(timeout=15) as client:
99
+ response = await client.post(f"{environment_url}{RPC_PATH}", json=body)
100
+ error = (response.json() or {}).get("error") or {}
101
+ except Exception as e:
102
+ logger.warning(f"operation probe {method} for '{self.env_id}' failed: {e}")
103
+ return None
104
+ if error.get("code") == -32601:
105
+ return False
106
+ if error.get("code") == -32000:
107
+ return None
108
+ return True
@@ -0,0 +1,376 @@
1
+ """Install an A2A agent onto a running task container by executing the agent's install/v1 extension commands.
2
+
3
+ install_commands use `{name}` placeholders (Python str.format) for everything that
4
+ varies per install — container name, work dir, agent build context, A2A port, and
5
+ config-sourced creds. The runner resolves each declared `required_params` entry
6
+ via the registry below, `shlex.quote`s the value, and `cmd.format(**resolved)`s
7
+ each command before exec. No host env vars are exported.
8
+
9
+ To add a new param: register it in `_build_param_resolvers` (and document what
10
+ it returns); the agent's extension declares it in `required_params` to opt in.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import logging
18
+ import shlex
19
+ from datetime import datetime, timezone
20
+ from typing import Any, Callable, ClassVar, Optional
21
+
22
+ import httpx
23
+
24
+ from agent_env.task_step.context import DeployedAgent, TaskStepContext
25
+ from agent_env.entity_refs import EntityRef
26
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ INSTALL_COMMANDS_KEY = "install_commands"
31
+ INSTALL_COMMANDS_HOST_KEY = "install_commands_host"
32
+ REQUIRED_PARAMS_KEY = "required_params"
33
+ REQUIRED_PARAMS_HOST_KEY = "required_params_host"
34
+ A2A_PORT_KEY = "a2a_port"
35
+
36
+ def _build_param_resolvers(
37
+ *,
38
+ container_name: Optional[str],
39
+ work_dir: str,
40
+ agent_ctx_tar: str,
41
+ a2a_port: int,
42
+ config,
43
+ agent_name: str,
44
+ workspace_dir: Optional[str] = None,
45
+ litellm_api_key_override: Optional[str] = None,
46
+ ) -> dict[str, Callable[[], str]]:
47
+ """Per-install registry: required_params name -> zero-arg resolver returning the raw value.
48
+
49
+ Anything not in this registry is rejected at install time with a clear error,
50
+ so an extension can't ask for a param the runner doesn't know how to source.
51
+ """
52
+
53
+ def _resolve_workspace_dir() -> str:
54
+ # The agent's container working directory (e.g. a Harbor task's
55
+ # `workdir`). Opt-in: an agent declares `workspace_dir` in
56
+ # required_params and uses `{workspace_dir}` in its install_commands
57
+ # (e.g. `docker exec -w {workspace_dir} ...`). Inert for agents that
58
+ # don't, so existing agents are unaffected.
59
+ if workspace_dir is None:
60
+ raise KeyError(
61
+ "agent declares required_param 'workspace_dir' but the InstallAgentTaskStep "
62
+ "was constructed without one; pass workspace_dir=<container working directory>"
63
+ )
64
+ return workspace_dir
65
+
66
+ def _resolve_container() -> str:
67
+ if container_name is None:
68
+ raise KeyError(
69
+ "agent declares required_param 'container' but this is a host-mode install "
70
+ "(InstallAgentTaskStep has no container_name)"
71
+ )
72
+ return container_name
73
+
74
+ return {
75
+ # Runner-supplied (from this step's own state)
76
+ "container": _resolve_container,
77
+ "work_dir": lambda: work_dir,
78
+ "agent_ctx_tar": lambda: agent_ctx_tar,
79
+ "a2a_port": lambda: str(a2a_port),
80
+ "agent_name": lambda: agent_name,
81
+ "workspace_dir": _resolve_workspace_dir,
82
+ # Config-sourced (from agent-env config)
83
+ "litellm_api_key": lambda: litellm_api_key_override or config.get_litellm_api_key(),
84
+ "litellm_base_url": lambda: config.get_litellm_base_url(),
85
+ }
86
+
87
+
88
+ class InstallAgentTaskStep(TaskStep):
89
+ type: ClassVar[str] = "install_agent"
90
+ entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
91
+
92
+ def __init__(
93
+ self,
94
+ id: str,
95
+ version: Optional[int],
96
+ sandbox_name: str,
97
+ container_name: Optional[str] = None,
98
+ a2a_agent_id: str = "",
99
+ a2a_agent_version: Optional[int] = None,
100
+ agent_name: str = TaskStep.DEFAULT_AGENT_NAME,
101
+ a2a_port: Optional[int] = None,
102
+ workspace_dir: Optional[str] = None,
103
+ depends_on: Optional[list[TaskStepDependency]] = None,
104
+ fail_task_on_error: bool = True,
105
+ ):
106
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
107
+ if not a2a_agent_id:
108
+ raise ValueError("a2a_agent_id is required")
109
+ self.sandbox_name = sandbox_name
110
+ self.container_name = container_name
111
+ self.a2a_agent_id = a2a_agent_id
112
+ self.a2a_agent_version = a2a_agent_version
113
+ self.agent_name = agent_name
114
+ self.a2a_port = a2a_port
115
+ self.workspace_dir = workspace_dir
116
+
117
+ def to_dict(self) -> dict:
118
+ base = super().to_dict()
119
+ base["sandbox_name"] = self.sandbox_name
120
+ base["container_name"] = self.container_name
121
+ base["a2a_agent_id"] = self.a2a_agent_id
122
+ base["a2a_agent_version"] = self.a2a_agent_version
123
+ base["agent_name"] = self.agent_name
124
+ base["a2a_port"] = self.a2a_port
125
+ base["workspace_dir"] = self.workspace_dir
126
+ return base
127
+
128
+ @classmethod
129
+ def from_dict(cls, data: dict) -> InstallAgentTaskStep:
130
+ return cls(
131
+ **cls._base_from_dict(data),
132
+ sandbox_name=data["sandbox_name"],
133
+ container_name=data.get("container_name"),
134
+ a2a_agent_id=data["a2a_agent_id"],
135
+ a2a_agent_version=data.get("a2a_agent_version"),
136
+ agent_name=data.get("agent_name", TaskStep.DEFAULT_AGENT_NAME),
137
+ a2a_port=data.get("a2a_port"),
138
+ workspace_dir=data.get("workspace_dir"),
139
+ )
140
+
141
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
142
+ from agent_env.a2a_agent import A2AAgent
143
+ from agent_env.providers.sandbox_providers.sandbox_provider import (
144
+ SANDBOX_MODE_VM,
145
+ build_sandbox_provider,
146
+ get_sandbox_provider,
147
+ )
148
+ from agent_env.config import ConfigError, get_config
149
+
150
+ existing = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
151
+ if existing is not None:
152
+ raise RuntimeError(f"Agent with name '{self.agent_name}' is already deployed")
153
+
154
+ if self.container_name is not None:
155
+ containers = context.metadata.get("deployed_docker_containers", [])
156
+ container = next(
157
+ (
158
+ c for c in containers
159
+ if c.get("container_name") == self.container_name
160
+ and c.get("sandbox_name") == self.sandbox_name
161
+ ),
162
+ None,
163
+ )
164
+ if container is None:
165
+ raise RuntimeError(
166
+ f"Container '{self.container_name}' not found on sandbox '{self.sandbox_name}' in "
167
+ f"context.metadata['deployed_docker_containers']. Run RunDockerContainerTaskStep first."
168
+ )
169
+
170
+ ds = next((s for s in context.deployed_sandboxes if s.sandbox_name == self.sandbox_name), None)
171
+ if ds is None:
172
+ raise RuntimeError(f"Sandbox '{self.sandbox_name}' not found in context.deployed_sandboxes")
173
+ if ds.sandbox_mode != SANDBOX_MODE_VM:
174
+ raise RuntimeError(
175
+ f"InstallAgent requires a VM-mode sandbox; '{self.sandbox_name}' is mode={ds.sandbox_mode!r}"
176
+ )
177
+
178
+ provider = (
179
+ build_sandbox_provider(ds.sandbox_type) if ds.sandbox_type else get_sandbox_provider()
180
+ )
181
+ sandbox = await provider.get_sandbox(ds.sandbox_id)
182
+
183
+ # Honor `--a2a-agent-id` override (same pattern as DeployAgentTaskStep) — lets the
184
+ # caller swap which agent gets installed without rewriting the task definition.
185
+ user_overrides = context.metadata.get("user_overrides", {})
186
+ a2a_agent_id = user_overrides.get("a2a_agent_id") or self.a2a_agent_id
187
+ agent = A2AAgent.get(a2a_agent_id, self.a2a_agent_version)
188
+ if not agent.docker_image_artifact.build_context_object_url:
189
+ raise RuntimeError(
190
+ f"Agent {agent.id} v{agent.version} has no build_context_object_url on its docker_image_artifact; "
191
+ f"install/v1 needs the build context (agent source files) to install into the task container"
192
+ )
193
+
194
+ logger.info(f"Loading agent image {agent.docker_image_artifact.image_name} onto VM (needed to read agent card)...")
195
+ await sandbox.load_docker_images([agent.docker_image_artifact])
196
+
197
+ install_ext = await self._extract_install_extension(sandbox, agent.docker_image_artifact.image_name)
198
+ params = install_ext.get("params") or {}
199
+ host_mode = self.container_name is None
200
+ commands_key = INSTALL_COMMANDS_HOST_KEY if host_mode else INSTALL_COMMANDS_KEY
201
+ install_commands = params.get(commands_key)
202
+ a2a_port = self.a2a_port or params.get(A2A_PORT_KEY)
203
+ required_params = params.get(REQUIRED_PARAMS_HOST_KEY if host_mode else REQUIRED_PARAMS_KEY) or []
204
+ if host_mode and not install_commands:
205
+ raise RuntimeError(
206
+ f"Agent {agent.id} v{agent.version} install/v1 params have no '{INSTALL_COMMANDS_HOST_KEY}'; "
207
+ f"this agent does not support host-mode install (pass container_name for container mode)"
208
+ )
209
+ if not isinstance(install_commands, list) or not install_commands:
210
+ raise RuntimeError(f"Agent {agent.id} v{agent.version} install/v1 params missing '{commands_key}' (list)")
211
+ if not isinstance(a2a_port, int):
212
+ raise RuntimeError(f"Agent {agent.id} v{agent.version} install/v1 params missing '{A2A_PORT_KEY}' (int) and the step has no a2a_port override")
213
+ if not isinstance(required_params, list):
214
+ raise RuntimeError(f"Agent {agent.id} v{agent.version} install/v1 params 'required_params' must be a list")
215
+
216
+ if not ds.tunnel_urls or str(a2a_port) not in ds.tunnel_urls:
217
+ raise RuntimeError(
218
+ f"Sandbox '{self.sandbox_name}' does not tunnel port {a2a_port} required by agent {agent.id}; "
219
+ f"include exposed_ports=[{a2a_port}] on DeploySandboxTaskStep"
220
+ + ("" if host_mode else f" and ports=[{a2a_port}] on RunDockerContainerTaskStep")
221
+ )
222
+ a2a_url = ds.tunnel_urls[str(a2a_port)]
223
+ self._check_tunnel_collision(context, ds.sandbox_id, a2a_url, a2a_port)
224
+
225
+ # Stage the agent build context onto the VM. The install_commands docker-cp from this dir.
226
+ work_dir = f"/tmp/install-agent-{self.agent_name}"
227
+ agent_ctx_tar = f"{work_dir}/agent-ctx.tar.gz"
228
+ await sandbox.exec_script(f"rm -rf {shlex.quote(work_dir)} && mkdir -p {shlex.quote(work_dir)}")
229
+ logger.info(f"Downloading agent build context {agent.docker_image_artifact.build_context_object_url} -> {agent_ctx_tar}")
230
+ await sandbox.load_s3_file(agent.docker_image_artifact.build_context_object_url, agent_ctx_tar)
231
+
232
+ # Resolve each required_param via the registry, shlex.quote so str.format
233
+ # substitution produces shell-safe tokens.
234
+ resolvers = _build_param_resolvers(
235
+ container_name=self.container_name,
236
+ work_dir=work_dir,
237
+ agent_ctx_tar=agent_ctx_tar,
238
+ a2a_port=a2a_port,
239
+ config=get_config(),
240
+ workspace_dir=self.workspace_dir,
241
+ agent_name=self.agent_name,
242
+ litellm_api_key_override=user_overrides.get("litellm_api_key"),
243
+ )
244
+ resolved: dict[str, str] = {}
245
+ for name in required_params:
246
+ if name not in resolvers:
247
+ raise RuntimeError(
248
+ f"Agent {agent.id} declares required_params={name!r} but no resolver is registered "
249
+ f"in InstallAgentTaskStep._build_param_resolvers. Known params: {sorted(resolvers)}"
250
+ )
251
+ try:
252
+ raw = resolvers[name]()
253
+ except (KeyError, ConfigError) as e:
254
+ raise RuntimeError(f"Failed to resolve required_param {name!r}: {e}") from e
255
+ resolved[name] = shlex.quote(raw)
256
+
257
+ target = "the VM host" if host_mode else f"container '{self.container_name}'"
258
+ logger.info(
259
+ f"Running {len(install_commands)} install command(s) against {target} "
260
+ f"on sandbox {ds.sandbox_id} (params resolved: {sorted(resolved)})"
261
+ )
262
+ for i, cmd in enumerate(install_commands, 1):
263
+ try:
264
+ formatted = cmd.format(**resolved)
265
+ except KeyError as e:
266
+ raise RuntimeError(
267
+ f"{commands_key}[{i-1}] references undeclared placeholder {{{e.args[0]}}}; "
268
+ f"add it to the extension's required_params"
269
+ )
270
+ logger.info(f" [{i}/{len(install_commands)}] {formatted.splitlines()[0][:140]}")
271
+ await sandbox.exec_script(formatted)
272
+
273
+ agent_card = await self._wait_for_agent_card(a2a_url)
274
+ logger.info(f"Agent '{self.agent_name}' reachable at {a2a_url} (card name={agent_card.get('name')})")
275
+
276
+ deployed = self._register_instance(agent, a2a_url, ds, agent_card)
277
+
278
+ context.deployed_agents.append(DeployedAgent(
279
+ agent_name=self.agent_name,
280
+ api_url=a2a_url,
281
+ sandbox_id=ds.sandbox_id,
282
+ sandbox_type=ds.sandbox_type,
283
+ a2a_url=a2a_url,
284
+ a2a_card=agent_card,
285
+ instance_id=deployed.instance_id,
286
+ on_host=host_mode,
287
+ ))
288
+
289
+ default_model = agent.metadata.get("default_model") or get_config().get_model_for_role("agent")
290
+ if default_model and not context.default_agent_model:
291
+ context.default_agent_model = default_model
292
+ logger.info(f"Set context.default_agent_model to '{default_model}'")
293
+
294
+ return context
295
+
296
+ def _register_instance(self, agent, a2a_url: str, ds, agent_card: dict):
297
+ """Register the installed agent in the instance store, same as A2AAgent.deploy(),
298
+ so store-resolving consumers (AddSkillsTaskStep, the a2a-agent CLIs) can find it."""
299
+ from agent_env.a2a_agent.a2a_agent import DeployedA2AAgent
300
+ from agent_env.a2a_agent.store import get_a2a_agent_instance_store
301
+
302
+ deployed = get_a2a_agent_instance_store().create_instance(
303
+ DeployedA2AAgent(
304
+ agent_id=agent.id,
305
+ agent_version=agent.version,
306
+ a2a_url=a2a_url,
307
+ sandbox_id=ds.sandbox_id,
308
+ agent_card=agent_card,
309
+ sandbox_type=ds.sandbox_type,
310
+ ),
311
+ ttl_seconds=self._instance_ttl_seconds(ds),
312
+ )
313
+ logger.info(f"Registered installed agent '{self.agent_name}' as instance {deployed.instance_id}")
314
+ return deployed
315
+
316
+ @staticmethod
317
+ def _instance_ttl_seconds(ds) -> int:
318
+ # The installed agent lives exactly as long as its host sandbox, so mirror the
319
+ # sandbox expiry when the context record carries one.
320
+ if ds.expires_at_utc:
321
+ try:
322
+ expires = datetime.strptime(ds.expires_at_utc, "%Y-%m-%d %H:%M UTC").replace(tzinfo=timezone.utc)
323
+ return max(60, int((expires - datetime.now(timezone.utc)).total_seconds()))
324
+ except ValueError:
325
+ pass
326
+ return TaskStep.DEFAULT_TTL_SECONDS
327
+
328
+ async def _extract_install_extension(self, sandbox, agent_image_name: str) -> dict:
329
+ """Run the agent image briefly with --entrypoint python3 to dump its AGENT_CARD and find the install/v1 extension."""
330
+ from agent_env.a2a_agent import A2AAgent
331
+
332
+ script = (
333
+ "import sys, json; sys.path.insert(0, '/app'); "
334
+ "from a2a_server import AGENT_CARD; "
335
+ "print(json.dumps(AGENT_CARD.model_dump()))"
336
+ )
337
+ exit_code, stdout, stderr = await sandbox.exec_with_output(
338
+ "sudo", "docker", "run", "--rm", "--entrypoint", "python3",
339
+ agent_image_name, "-c", script,
340
+ )
341
+ if exit_code != 0:
342
+ raise RuntimeError(f"Failed to extract AGENT_CARD from {agent_image_name}: {stderr or stdout}")
343
+ try:
344
+ card = json.loads(stdout)
345
+ except json.JSONDecodeError as e:
346
+ raise RuntimeError(f"Could not parse AGENT_CARD JSON from {agent_image_name}: {e}; stdout={stdout[:500]!r}")
347
+ install_ext = A2AAgent.find_extension(card, A2AAgent.EXT_INSTALL)
348
+ if install_ext is None:
349
+ raise RuntimeError(
350
+ f"Agent image {agent_image_name} does not declare {A2AAgent.EXT_INSTALL} on its card; "
351
+ f"cannot use InstallAgentTaskStep with this agent"
352
+ )
353
+ return install_ext
354
+
355
+ def _check_tunnel_collision(self, context: TaskStepContext, sandbox_id: Optional[str], a2a_url: str, a2a_port: int) -> None:
356
+ # same sandbox + same port => identical tunnel URL
357
+ for other in context.deployed_agents:
358
+ if other.sandbox_id == sandbox_id and other.api_url == a2a_url:
359
+ raise RuntimeError(f"Tunnel for port {a2a_port} ({a2a_url}) already serves deployed agent '{other.agent_name}' on sandbox {sandbox_id}; pass a distinct a2a_port on this step")
360
+
361
+ async def _wait_for_agent_card(self, a2a_url: str, timeout_s: int = 300, poll_s: int = 5) -> dict:
362
+ deadline = asyncio.get_event_loop().time() + timeout_s
363
+ last_err: Exception | None = None
364
+ async with httpx.AsyncClient() as client:
365
+ while asyncio.get_event_loop().time() < deadline:
366
+ try:
367
+ resp = await client.get(f"{a2a_url}/.well-known/agent.json", timeout=10)
368
+ if resp.status_code == 200:
369
+ return resp.json()
370
+ except Exception as e:
371
+ last_err = e
372
+ await asyncio.sleep(poll_s)
373
+ raise RuntimeError(
374
+ f"Agent card at {a2a_url}/.well-known/agent.json did not respond within {timeout_s}s "
375
+ f"(last error: {type(last_err).__name__}: {last_err})"
376
+ )