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,241 @@
1
+ """Verify universe load/export roundtrip for MultiEnv compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import os
9
+ import tempfile
10
+ from typing import TYPE_CHECKING, Any, ClassVar, Optional
11
+
12
+ from agent_env.env.env_artifact_store import EnvArtifactType, get_env_artifact_store
13
+ from agent_env.artifact.store import artifact_write_lock
14
+ from agent_env.task_step.context import TaskStepContext
15
+ from agent_env.entity_refs import EntityRef
16
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
17
+
18
+ from .universe_comparison import classify_issues, compare_dicts, normalize
19
+
20
+ if TYPE_CHECKING:
21
+ from agent_env.env.env import DeployedEnv
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class VerifyUniverseLoadExportRoundtripStep(TaskStep):
27
+ type: ClassVar[str] = "verify_universe_load_export_roundtrip"
28
+ entity_refs = (
29
+ EntityRef.env("env_id"),
30
+ EntityRef.artifact("universe_artifact_id", version_field="universe_artifact_version", artifact_type="environment_universe"),
31
+ )
32
+
33
+ def __init__(
34
+ self,
35
+ id: str,
36
+ version: Optional[int],
37
+ env_id: str,
38
+ universe_artifact_id: str,
39
+ universe_artifact_version: Optional[int] = None,
40
+ emit_file_artifact_universe: bool = False,
41
+ persist_result: bool = True,
42
+ depends_on: Optional[list[TaskStepDependency]] = None,
43
+ fail_task_on_error: bool = True,
44
+ ):
45
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
46
+ self.env_id = env_id
47
+ self.universe_artifact_id = universe_artifact_id
48
+ self.universe_artifact_version = universe_artifact_version
49
+ # When set, also bundle the original/export1/export2 JSON into a FileArtifactUniverse so a
50
+ # downstream agent-judge can diff them on its filesystem. Off by default — the
51
+ # programmatic verdict does not need it. The bundle's id is deterministic (see
52
+ # file_artifact_universe_id) so the callsite can reference it statically.
53
+ self.emit_file_artifact_universe = emit_file_artifact_universe
54
+ # When False, compute the programmatic verdict into context but DON'T persist it to the
55
+ # env-artifact store — a downstream step (CombineUniverseVerdictsStep) becomes the single
56
+ # authoritative writer, so a crash before the judge merges can't leave an ungated half-write
57
+ # masquerading as final. Defaults True to preserve the step's standalone contract.
58
+ self.persist_result = persist_result
59
+
60
+ def to_dict(self) -> dict:
61
+ base = super().to_dict()
62
+ base["env_id"] = self.env_id
63
+ base["universe_artifact_id"] = self.universe_artifact_id
64
+ base["universe_artifact_version"] = self.universe_artifact_version
65
+ base["emit_file_artifact_universe"] = self.emit_file_artifact_universe
66
+ base["persist_result"] = self.persist_result
67
+ return base
68
+
69
+ @classmethod
70
+ def from_dict(cls, data: dict) -> VerifyUniverseLoadExportRoundtripStep:
71
+ return cls(
72
+ **cls._base_from_dict(data),
73
+ env_id=data["env_id"],
74
+ universe_artifact_id=data["universe_artifact_id"],
75
+ universe_artifact_version=data.get("universe_artifact_version"),
76
+ emit_file_artifact_universe=data.get("emit_file_artifact_universe", False),
77
+ persist_result=data.get("persist_result", True),
78
+ )
79
+
80
+ @staticmethod
81
+ def file_artifact_universe_id(env_id: str, env_version: int, universe_id: str, universe_version: int) -> str:
82
+ """Deterministic id of the emitted FileArtifactUniverse, so the callsite can reference it
83
+ statically without a runtime context lookup (mirrors the export1 artifact id scheme)."""
84
+ return f"validate-{env_id}-v{env_version}-{universe_id}-v{universe_version}-fau"
85
+
86
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
87
+ from agent_env.artifact import EnvironmentUniverseArtifact
88
+ from agent_env.env.env import Env
89
+
90
+ # Phase 1: Resolve deployed env and universe artifact
91
+ deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
92
+ if deployed is None:
93
+ raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
94
+ env = Env.get(deployed.env_id, deployed.env_version)
95
+ env = await type(env).from_deployed_env(deployed)
96
+ universe = EnvironmentUniverseArtifact.get(self.universe_artifact_id, self.universe_artifact_version)
97
+ environment_artifacts = universe.get_environment_artifacts()
98
+ environment_names = [sa.environment_name for sa in environment_artifacts]
99
+
100
+ # Phase 2: Load original universe + read original data for comparison
101
+ logger.info(f"Loading universe {self.universe_artifact_id} into env {self.env_id}...")
102
+ await env.load_environment_universe_artifact(universe)
103
+ originals = {}
104
+ for sa in environment_artifacts:
105
+ try:
106
+ data = await asyncio.to_thread(lambda sa=sa: sa.get_file_artifact().load())
107
+ originals[sa.environment_name] = normalize(json.loads(data))
108
+ except (json.JSONDecodeError, UnicodeDecodeError):
109
+ logger.info(f"{sa.environment_name}: non-JSON artifact, will only compare export1 vs export2")
110
+
111
+ # Phase 3: Export #1
112
+ logger.info("Exporting state (round 1)...")
113
+ export1_raw = await self._export_all(deployed, environment_names)
114
+ export1 = {name: normalize(data) for name, data in export1_raw.items()}
115
+
116
+ # Phase 4: Create artifacts from export #1
117
+ logger.info("Creating artifacts from export #1...")
118
+ exported_universe = await asyncio.to_thread(
119
+ self._create_universe_artifact, environment_artifacts, export1_raw, deployed.env_version, universe.version
120
+ )
121
+
122
+ # Phase 5: Reload from exported artifacts
123
+ logger.info("Reloading from exported artifacts...")
124
+ await env.load_environment_universe_artifact(exported_universe)
125
+
126
+ # Phase 6: Export #2
127
+ logger.info("Exporting state (round 2)...")
128
+ export2_raw = await self._export_all(deployed, environment_names)
129
+ export2 = {name: normalize(data) for name, data in export2_raw.items()}
130
+
131
+ # Phase 7: Compare
132
+ logger.info("Comparing results...")
133
+ # "environments" is the SAME dict object as "services" (dual-write), so every later
134
+ # mutation — the loop below, and apply_judge_verdict's in-place merge — lands in both.
135
+ per_environment: dict[str, Any] = {}
136
+ result: dict[str, Any] = {"compatible": True, "services": per_environment, "environments": per_environment, "exported_universe_artifact_id": exported_universe.id}
137
+ for name in environment_names:
138
+ load_issues = compare_dicts(originals[name], export1[name], "universe", "export1") if name in originals else []
139
+ idempotency_issues = compare_dicts(export1[name], export2[name], "export1", "export2")
140
+ annotated, is_compatible = classify_issues(load_issues, idempotency_issues)
141
+ if not is_compatible:
142
+ result["compatible"] = False
143
+ result["services"][name] = {"compatible": is_compatible, "issues": annotated}
144
+ critical_count = sum(1 for i in annotated if i["critical"])
145
+ logger.info(f" {name}: {'COMPATIBLE' if is_compatible else 'INCOMPATIBLE'} ({len(annotated)} issues, {critical_count} critical)")
146
+
147
+ # Phase 7b: Optionally bundle original/export1/export2 JSON into a FileArtifactUniverse so a
148
+ # downstream agent-judge can diff them on its filesystem. Additive — does not touch
149
+ # the programmatic verdict above.
150
+ if self.emit_file_artifact_universe:
151
+ fau = await asyncio.to_thread(
152
+ self._create_file_artifact_universe,
153
+ environment_artifacts, exported_universe, export2_raw, deployed.env_version, universe.version,
154
+ )
155
+ result["file_artifact_universe_id"] = fau.id
156
+ logger.info(f"Emitted FileArtifactUniverse for agent-judge: {fau.id}")
157
+
158
+ # Phase 8: Always expose the programmatic result in context (downstream steps read it);
159
+ # only persist to the env-artifact store when this step owns the write (persist_result).
160
+ # When a CombineUniverseVerdictsStep follows, it is the single authoritative writer.
161
+ context.metadata.setdefault("verifications", {})[EnvArtifactType.UNIVERSE_COMPATIBILITY] = result
162
+ if self.persist_result:
163
+ get_env_artifact_store().put(env_id=self.env_id, env_version=deployed.env_version, artifact_id=universe.id, artifact_version=universe.version, type=EnvArtifactType.UNIVERSE_COMPATIBILITY, data=result)
164
+
165
+ return context
166
+
167
+ def _create_file_artifact_universe(self, original_environment_artifacts: list, exported_universe: Any, export2_raw: dict[str, dict], env_version: int, universe_version: int) -> Any:
168
+ """Bundle the three universe snapshots into one FileArtifactUniverse for the agent-judge.
169
+
170
+ Faithfulness (this validator measures lossiness, so the bundle must not transform data):
171
+ - ``original/<svc>.json`` references the universe's EXISTING per-service FileArtifacts
172
+ (verbatim bytes — what compare_dicts loaded), no re-serialization.
173
+ - ``export_1/<svc>.json`` references the FileArtifacts the export1 universe was built from
174
+ (the exact bytes the programmatic verdict used).
175
+ - ``export_2/<svc>.json`` is materialized fresh (export2 is not otherwise persisted), dumped
176
+ from the same in-memory raw dict, with the same serialization as export1
177
+ (``default=str`` for parity; ``ensure_ascii=False`` so the judge reads real unicode).
178
+ """
179
+ from agent_env.artifact import FileArtifact, FileArtifactUniverse
180
+
181
+ prefix = f"validate-{self.env_id}-v{env_version}-{self.universe_artifact_id}-v{universe_version}"
182
+ with artifact_write_lock(prefix):
183
+ file_artifacts: dict[str, Any] = {}
184
+ for sa in original_environment_artifacts:
185
+ file_artifacts[f"original/{sa.environment_name}.json"] = sa.get_file_artifact()
186
+ for sa in exported_universe.get_environment_artifacts():
187
+ file_artifacts[f"export_1/{sa.environment_name}.json"] = sa.get_file_artifact()
188
+ for name, data in export2_raw.items():
189
+ # Cleanup wraps both the dump and the put, so a failure in either can't leak the temp
190
+ # file; mkstemp yields the path up front so `finally` always has a valid path to unlink.
191
+ fd, tmp_path = tempfile.mkstemp(suffix=".json", prefix=f"export2-{name}-")
192
+ try:
193
+ with os.fdopen(fd, "w") as f:
194
+ json.dump(data, f, default=str, ensure_ascii=False)
195
+ fa = FileArtifact.put(id=f"{prefix}-export2-{name}", description=f"Export #2 of {name} for universe-judge", file_path=tmp_path)
196
+ finally:
197
+ os.unlink(tmp_path)
198
+ file_artifacts[f"export_2/{name}.json"] = fa
199
+
200
+ return FileArtifactUniverse.put(
201
+ id=self.file_artifact_universe_id(self.env_id, env_version, self.universe_artifact_id, universe_version),
202
+ file_artifacts=file_artifacts,
203
+ )
204
+
205
+ @staticmethod
206
+ async def _export_all(deployed: DeployedEnv, environment_names: list[str]) -> dict[str, dict]:
207
+ """Export each service: v1 ``data/get`` at the base the env card gives it, else legacy ``GET /export-state``."""
208
+ from agent_env.env import legacy_protocol
209
+ from agent_env.env.env import gateway_url_of
210
+ from agentenv_protocol import client as protocol_v1
211
+ result = {}
212
+ for name in environment_names:
213
+ base_url = await legacy_protocol.v1_base_url(deployed, gateway_url_of(deployed), name)
214
+ if base_url is not None:
215
+ resp = await protocol_v1.get_data(base_url)
216
+ result[name] = resp.parts[0].data if resp.parts else {}
217
+ else:
218
+ result[name] = await legacy_protocol.export_state(gateway_url_of(deployed), name)
219
+ return result
220
+
221
+ def _create_universe_artifact(self, original_environment_artifacts: list, export_data: dict[str, dict], env_version: int, universe_version: int) -> Any:
222
+ """Create FileArtifact + EnvironmentArtifact per service, bundle into EnvironmentUniverseArtifact."""
223
+ from agent_env.artifact import FileArtifact, EnvironmentArtifact, EnvironmentUniverseArtifact
224
+ prefix = f"validate-{self.env_id}-v{env_version}-{self.universe_artifact_id}-v{universe_version}"
225
+ with artifact_write_lock(prefix):
226
+ export_environment_artifacts = []
227
+ for sa in original_environment_artifacts:
228
+ name = sa.environment_name
229
+ if name not in export_data:
230
+ continue
231
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, prefix=f"export1-{name}-") as f:
232
+ json.dump(export_data[name], f, default=str)
233
+ tmp_path = f.name
234
+ try:
235
+ file_artifact = FileArtifact.put(id=f"{prefix}-export1-{name}", description=f"Export #1 of {name} for universe compat validation", file_path=tmp_path)
236
+ finally:
237
+ os.unlink(tmp_path)
238
+ svc_artifact = EnvironmentArtifact.put(id=f"{prefix}-export1-svc-{name}", environment_name=sa.environment_name, file_artifact=file_artifact)
239
+ export_environment_artifacts.append(svc_artifact)
240
+
241
+ return EnvironmentUniverseArtifact.put(id=f"{prefix}-export1", environment_artifacts=export_environment_artifacts)
@@ -0,0 +1,92 @@
1
+ """Push routing tables to deployed A2A agents' /ext/peer-agents extension."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import dataclasses
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from typing import ClassVar, Optional
9
+
10
+ import httpx
11
+
12
+ from agent_env.task_step.context import DeployedAgent, TaskStepContext
13
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
14
+
15
+ logger = logging.getLogger(__name__)
16
+ _POST_TIMEOUT_SECONDS = 30
17
+
18
+
19
+ @dataclass
20
+ class AgentPeers:
21
+ source_agent_name: str
22
+ peer_agent_names: list[str]
23
+
24
+ @classmethod
25
+ def from_dict(cls, data: dict) -> "AgentPeers":
26
+ return cls(source_agent_name=data["source_agent_name"], peer_agent_names=list(data["peer_agent_names"]))
27
+
28
+
29
+ class PeerAgentsTaskStep(TaskStep):
30
+ type: ClassVar[str] = "peer_agents"
31
+ entity_refs = ()
32
+
33
+ def __init__(self, id: str, version: Optional[int], peerings: list, depends_on: Optional[list[TaskStepDependency]] = None, fail_task_on_error: bool = True):
34
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
35
+ self.peerings = [p if isinstance(p, AgentPeers) else AgentPeers.from_dict(p) for p in peerings]
36
+
37
+ def to_dict(self) -> dict:
38
+ return {**super().to_dict(), "peerings": [dataclasses.asdict(p) for p in self.peerings]}
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: dict) -> "PeerAgentsTaskStep":
42
+ return cls(**cls._base_from_dict(data), peerings=[AgentPeers.from_dict(p) for p in data["peerings"]])
43
+
44
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
45
+ from agent_env.a2a_agent import A2AAgent
46
+ if not self.peerings:
47
+ raise RuntimeError("PeerAgentsTaskStep: no peerings supplied")
48
+ deployed_by_name = {a.agent_name: a for a in context.deployed_agents}
49
+ self._validate(deployed_by_name, A2AAgent)
50
+ await asyncio.gather(*[self._post(deployed_by_name, p, A2AAgent) for p in self.peerings])
51
+ recorded = context.metadata.setdefault("agent_peerings", [])
52
+ for p in self.peerings:
53
+ recorded.append({"source_agent_name": p.source_agent_name, "peer_agent_names": list(p.peer_agent_names)})
54
+ logger.info("PeerAgentsTaskStep: peered %d source(s)", len(self.peerings))
55
+ return context
56
+
57
+ def _validate(self, deployed_by_name: dict[str, DeployedAgent], a2a_agent_cls) -> None:
58
+ seen: set[str] = set()
59
+ for p in self.peerings:
60
+ src = p.source_agent_name
61
+ if src in seen:
62
+ raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' listed twice")
63
+ seen.add(src)
64
+ agent = deployed_by_name.get(src)
65
+ if agent is None:
66
+ raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' not in deployed_agents (available: {sorted(deployed_by_name)})")
67
+ if not (agent.a2a_url or agent.api_url):
68
+ raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' has no a2a_url")
69
+ if a2a_agent_cls.find_extension(agent.a2a_card or {}, a2a_agent_cls.EXT_PEER_AGENTS) is None:
70
+ raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' does not advertise the peer-agents extension")
71
+ if src in p.peer_agent_names:
72
+ raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' lists itself as a peer")
73
+ missing = [n for n in p.peer_agent_names if n not in deployed_by_name]
74
+ if missing:
75
+ raise RuntimeError(f"PeerAgentsTaskStep: peers {missing} for source '{src}' not in deployed_agents")
76
+
77
+ async def _post(self, deployed_by_name: dict[str, DeployedAgent], peering: AgentPeers, a2a_agent_cls) -> None:
78
+ src = deployed_by_name[peering.source_agent_name]
79
+ a2a_url = src.a2a_url or src.api_url
80
+ ext = a2a_agent_cls.find_extension(src.a2a_card or {}, a2a_agent_cls.EXT_PEER_AGENTS)
81
+ endpoint = (ext.get("params") or {}).get("endpoint", "/ext/peer-agents")
82
+ peers = [self._record(deployed_by_name[n]) for n in peering.peer_agent_names]
83
+ async with httpx.AsyncClient() as client:
84
+ resp = await client.post(a2a_url + endpoint, json={"peers": peers}, timeout=_POST_TIMEOUT_SECONDS)
85
+ if resp.status_code >= 400:
86
+ raise RuntimeError(f"PeerAgentsTaskStep: POST {a2a_url + endpoint} failed for '{peering.source_agent_name}': {resp.status_code} {resp.text}")
87
+ logger.info("PeerAgentsTaskStep: peered '%s' with %s", peering.source_agent_name, peering.peer_agent_names)
88
+
89
+ @staticmethod
90
+ def _record(peer: DeployedAgent) -> dict:
91
+ card = peer.a2a_card or {}
92
+ return {"name": peer.agent_name, "url": peer.a2a_url or peer.api_url, "card": card, "description": card.get("description")}