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,901 @@
1
+ """A2A agent validator — orchestrates the full validation flow.
2
+
3
+ Extracted from `A2AAgent.validate()` so the data model (`A2AAgent`) and the
4
+ validation behavior live in separate modules.
5
+
6
+ Adding a new modality probe is one new `ModalityProbe` entry in
7
+ `MODALITY_PROBES`. The step ID, prompt ID, `PromptAgentTaskStep`
8
+ instantiation, and grading-list entry are all derived from that single
9
+ tuple — no need to coordinate three places.
10
+
11
+ Use as `await A2AAgentValidator.validate(agent)`. `A2AAgent.validate()` is
12
+ a thin wrapper that delegates here.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import base64
18
+ import logging
19
+ import uuid
20
+ from dataclasses import dataclass
21
+ from typing import Callable, Optional, TYPE_CHECKING, Union
22
+
23
+ from agentenv_protocol.a2a_agent import ObjectChangelogApplyResponse
24
+
25
+ from agent_env.a2a_agent.object_transfer import (
26
+ TRANSFER_TIMEOUT_SECONDS,
27
+ changelog_apply_call,
28
+ check_changelog_applied,
29
+ invoke_transfer,
30
+ )
31
+ from agent_env.config import get_config
32
+ from agent_env.store.routing import refuse_local_derivation
33
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_modalities import (
34
+ AUDIO_M4A_EXPECTED,
35
+ AUDIO_M4A_PROBE_PARTS,
36
+ AUDIO_MP3_EXPECTED,
37
+ AUDIO_MP3_PROBE_PARTS,
38
+ AUDIO_OGG_EXPECTED,
39
+ AUDIO_OGG_PROBE_PARTS,
40
+ AUDIO_WAV_EXPECTED,
41
+ AUDIO_WAV_PROBE_PARTS,
42
+ IMAGE_GIF_PROBE_PARTS,
43
+ IMAGE_JPEG_PROBE_PARTS,
44
+ IMAGE_PROBE_EXPECTED,
45
+ IMAGE_PROBE_PARTS,
46
+ IMAGE_PROBE_PNG_B64,
47
+ IMAGE_PROBE_PROMPT,
48
+ PDF_PROBE_EXPECTED,
49
+ PDF_PROBE_PARTS,
50
+ TEXT_PROBE_EXPECTED,
51
+ TEXT_PROBE_PARTS,
52
+ VIDEO_PROBE_EXPECTED,
53
+ VIDEO_PROBE_MP4_B64,
54
+ VIDEO_PROBE_PROMPT,
55
+ )
56
+
57
+ if TYPE_CHECKING:
58
+ from agent_env.a2a_agent.a2a_agent import A2AAgent
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+
63
+ @dataclass
64
+ class _UploadedFixtures:
65
+ """Object-store-hosted probe fixtures created at validation runtime."""
66
+ skill_object_url: str
67
+ png_object_uri: str
68
+ png_signed_url: str
69
+ mp4_object_uri: str
70
+
71
+
72
+ _PartsFactory = Callable[[_UploadedFixtures], list[dict]]
73
+
74
+
75
+ @dataclass
76
+ class ModalityProbe:
77
+ """One modality probe in the validator flow.
78
+
79
+ `parts` is either a static list of A2A parts (for inline probes whose
80
+ bytes are known at module load) or a callable that takes the runtime
81
+ `_UploadedFixtures` and returns parts (for URI probes whose s3:// or
82
+ presigned https:// URIs only exist after upload).
83
+
84
+ Each `ModalityProbe` produces exactly one `PromptAgentTaskStep` and
85
+ one entry in `VerifyA2AModalitiesStep.probes`. Step ID, prompt ID,
86
+ and grading entry all derive from `prompt_id_suffix`.
87
+ """
88
+ modality: str
89
+ prompt_id_suffix: str
90
+ parts: Union[list[dict], _PartsFactory]
91
+ expected: str
92
+ timeout_s: int = 120
93
+
94
+
95
+ MODALITY_PROBES: list[ModalityProbe] = [
96
+ ModalityProbe("text", "modality-text", TEXT_PROBE_PARTS, TEXT_PROBE_EXPECTED),
97
+ ModalityProbe("image/png", "modality-image", IMAGE_PROBE_PARTS, IMAGE_PROBE_EXPECTED),
98
+ ModalityProbe("image/jpeg", "modality-image-jpeg", IMAGE_JPEG_PROBE_PARTS, IMAGE_PROBE_EXPECTED),
99
+ ModalityProbe("image/gif", "modality-image-gif", IMAGE_GIF_PROBE_PARTS, IMAGE_PROBE_EXPECTED),
100
+ ModalityProbe("audio/wav", "modality-audio-wav", AUDIO_WAV_PROBE_PARTS, AUDIO_WAV_EXPECTED),
101
+ ModalityProbe("audio/mpeg", "modality-audio-mp3", AUDIO_MP3_PROBE_PARTS, AUDIO_MP3_EXPECTED),
102
+ ModalityProbe("audio/mp4", "modality-audio-m4a", AUDIO_M4A_PROBE_PARTS, AUDIO_M4A_EXPECTED),
103
+ ModalityProbe("audio/ogg", "modality-audio-ogg", AUDIO_OGG_PROBE_PARTS, AUDIO_OGG_EXPECTED),
104
+ ModalityProbe("application/pdf", "modality-pdf", PDF_PROBE_PARTS, PDF_PROBE_EXPECTED),
105
+ ModalityProbe(
106
+ "image/png+uri-s3", "modality-image-uri-s3",
107
+ lambda f: [
108
+ {"kind": "text", "text": IMAGE_PROBE_PROMPT},
109
+ {"kind": "file", "file": {"uri": f.png_object_uri, "mimeType": "image/png", "name": "red.png"}},
110
+ ],
111
+ IMAGE_PROBE_EXPECTED,
112
+ ),
113
+ ModalityProbe(
114
+ "image/png+uri-https", "modality-image-uri-https",
115
+ lambda f: [
116
+ {"kind": "text", "text": IMAGE_PROBE_PROMPT},
117
+ {"kind": "file", "file": {"uri": f.png_signed_url, "mimeType": "image/png", "name": "red.png"}},
118
+ ],
119
+ IMAGE_PROBE_EXPECTED,
120
+ ),
121
+ ModalityProbe(
122
+ "video/mp4", "modality-video",
123
+ lambda f: [
124
+ {"kind": "text", "text": VIDEO_PROBE_PROMPT},
125
+ {"kind": "file", "file": {"uri": f.mp4_object_uri, "mimeType": "video/mp4", "name": "clip.mp4"}},
126
+ ],
127
+ VIDEO_PROBE_EXPECTED,
128
+ timeout_s=180,
129
+ ),
130
+ ]
131
+
132
+
133
+ class A2AAgentValidator:
134
+ """Static namespace for running the full A2A agent validation flow."""
135
+
136
+ @staticmethod
137
+ async def validate(
138
+ agent: "A2AAgent",
139
+ on_progress: Optional[Callable[[str], None]] = None,
140
+ litellm_api_key: Optional[str] = None,
141
+ ) -> dict:
142
+ """Run the A2A agent validator task: deploy env + agent, verify card,
143
+ modality probes, MCP tools, skills, snapshot extension, etc.
144
+
145
+ Returns the merged `verifications` dict from the run's task context.
146
+ """
147
+ refuse_local_derivation(agent.id, "agent", "validating")
148
+ from agent_env.task import Task
149
+ from agent_env.task_step import (
150
+ AddSkillsTaskStep,
151
+ DeployAgentTaskStep,
152
+ DeployEnvTaskStep,
153
+ PromptAgentTaskStep,
154
+ RubricsVerifierTaskStep,
155
+ )
156
+ from agent_env.task_step.context import TaskStepContext
157
+ from agent_env.task_step.task_steps.a2a_agent_validator import (
158
+ VerifyA2AAgentCardStep,
159
+ VerifyA2AAgentConfigIdentityStep,
160
+ VerifyA2AAgentMCPStep,
161
+ VerifyA2ALitellmAttributionRuntimeStep,
162
+ VerifyA2ALitellmAttributionStep,
163
+ VerifyA2AModalitiesStep,
164
+ VerifyA2APeerAgentsStep,
165
+ VerifyA2ARoleStep,
166
+ VerifyA2ASkillConfigStep,
167
+ VerifyA2ASnapshotStep,
168
+ VerifyA2ASystemPromptStep,
169
+ VerifyA2ATrajectoryStep,
170
+ VerifyCoreA2AProtocolStep,
171
+ )
172
+ from agent_env.task_step.task_steps.add_skills import Skill
173
+ from agent_env.task_step.task_steps.peer_agents import AgentPeers, PeerAgentsTaskStep
174
+ from agent_env.task_step.task_steps.snapshot_agent_state import SnapshotAgentStateTaskStep
175
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
176
+ from agent_env.a2a_agent.a2a_agent import A2AAgent
177
+
178
+ task_id = f"validate-a2a-{agent.id}-v{agent.version}"
179
+
180
+ # ── Upload object-store fixtures (skill + URI probe bytes) ───────────
181
+ skill_object_url = await asyncio.to_thread(
182
+ A2AAgentValidator._upload_skill_fixture,
183
+ agent,
184
+ name="validator-test-s3",
185
+ description=(
186
+ "Validator test skill with project codename. "
187
+ "Use when asked about the project codename."
188
+ ),
189
+ body=(
190
+ "When asked for the project codename, respond with: "
191
+ "VALIDATOR-S3-99"
192
+ ),
193
+ )
194
+ skill_bundle_probe_url = await asyncio.to_thread(
195
+ A2AAgentValidator._upload_skill_fixture,
196
+ agent,
197
+ name="validator-probe-bundle",
198
+ description="Validator probe for the portable skill bundle variant.",
199
+ body="Portable skill bundle validation fixture.",
200
+ )
201
+ skill_s3_probe_url = await asyncio.to_thread(
202
+ A2AAgentValidator._upload_skill_fixture,
203
+ agent,
204
+ name="validator-probe-s3",
205
+ description="Validator probe for the legacy S3 skill variant.",
206
+ body="Legacy S3 skill validation fixture.",
207
+ )
208
+ fixtures = await asyncio.to_thread(A2AAgentValidator._upload_probe_fixtures, agent, skill_object_url)
209
+
210
+ # ── Build modality probe steps + matching grading list ───────────────
211
+ deploy_agent_id = f"{task_id}-deploy-agent"
212
+ modality_steps, grading_probes = A2AAgentValidator._build_modality_steps(task_id, fixtures, deploy_agent_id)
213
+ modality_step_ids = [s.id for s in modality_steps]
214
+
215
+ # ── Identifiers for non-modality steps ───────────────────────────────
216
+ protocol_prompt_id = f"{task_id}-protocol-prompt"
217
+ skill_prompt_id = f"{task_id}-skill-prompt"
218
+ skill_verifier_id = f"{task_id}-skill-rubric"
219
+ # LiteLLM attribution runtime probe: plant unique IDs on context.metadata
220
+ # so prompt_agent forwards them via /ext/agent-config; the agent records
221
+ # the attribution it emits on the resulting LLM call; the post-prompt
222
+ # verify step queries /ext/attribution-probe and asserts the planted
223
+ # values came through. Unique per validation run to avoid cross-run
224
+ # bleed when /ext/attribution-probe is stateful.
225
+ probe_project_id = f"probe-project-{uuid.uuid4().hex[:12]}"
226
+ probe_task_id = f"probe-task-{uuid.uuid4().hex[:12]}"
227
+ # Snapshot validation: plant a unique token on agent A, snapshot, deploy
228
+ # agent B with the snapshot loaded into a known target context_id, prompt
229
+ # agent B for recall, grade with a rubric.
230
+ snapshot_token = f"SNAPSHOT-TOKEN-{uuid.uuid4().hex[:12]}"
231
+ snapshot_plant_prompt_id = f"{task_id}-snapshot-plant-prompt"
232
+ snapshot_artifact_id = f"{task_id}-snapshot"
233
+ snapshot_target_agent_name = "snapshot-target-agent"
234
+ snapshot_target_context_id = uuid.uuid4().hex
235
+ snapshot_recall_prompt_id = f"{task_id}-snapshot-recall-prompt"
236
+ snapshot_recall_verifier_id = f"{task_id}-snapshot-recall-rubric"
237
+ snapshot_recall_criterion_id = "snapshot_recall_token"
238
+ # Peer-agents validation: deploy a second agent, peer it with the main one,
239
+ # have main relay a unique token from the peer, verify substring match.
240
+ peer_target_agent_name = "peer-target-agent"
241
+ peer_prompt_id = f"{task_id}-peer-prompt"
242
+ peer_token = f"PEERTOKEN-{uuid.uuid4().hex[:12]}"
243
+
244
+ deploy_agent_dep = [TaskStepDependency(task_step_id=deploy_agent_id)]
245
+ mcp_step_id = f"{task_id}-mcp"
246
+ skill_chain_id = f"{task_id}-add-skill"
247
+ snapshot_capture_id = f"{task_id}-snapshot-capture"
248
+ snapshot_target_deploy_id = f"{task_id}-deploy-snapshot-target"
249
+ snapshot_recall_rubric_id = f"{task_id}-snapshot-recall-rubric-step"
250
+ peer_target_deploy_id = f"{task_id}-deploy-peer-target"
251
+ peer_setup_id = f"{task_id}-peer-setup"
252
+ skill_rubric_id = f"{task_id}-skill-rubric-step"
253
+ # agent-changelog validation: a dedicated capture agent (enable_agent_changelog)
254
+ # writes a marker file via a tool call; the validator then drives apply
255
+ # onto a fresh agent (post-DAG) and checks the marker was reconstructed.
256
+ changelog_capture_agent_name = "changelog-capture-agent"
257
+ changelog_apply_agent_name = "changelog-apply-agent"
258
+ changelog_token = f"CHANGELOG-TOKEN-{uuid.uuid4().hex[:12]}"
259
+ changelog_marker_path = "/app/changelog_marker.txt"
260
+ changelog_capture_deploy_id = f"{task_id}-changelog-capture-deploy"
261
+ changelog_plant_prompt_id = f"{task_id}-changelog-plant"
262
+ changelog_apply_deploy_id = f"{task_id}-changelog-apply-deploy"
263
+ # install/v1 is validated post-DAG in _validate_install(), gated on the
264
+ # LIVE agent card (context.deployed_agents), so it never depends on a
265
+ # prior validation run having populated agent.metadata["agent_card"].
266
+
267
+ task = Task.put(id=task_id, steps=[
268
+ DeployEnvTaskStep(id=f"{task_id}-deploy-env", version=None, env_id=agent.VALIDATION_ENV_ID),
269
+ DeployAgentTaskStep(id=deploy_agent_id, version=None, env_ids=[agent.VALIDATION_ENV_ID], a2a_agent_id=agent.id, a2a_agent_version=agent.version),
270
+ VerifyA2AAgentCardStep(id=f"{task_id}-card", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
271
+ VerifyA2ALitellmAttributionStep(id=f"{task_id}-litellm-attribution", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
272
+ VerifyA2AAgentConfigIdentityStep(id=f"{task_id}-agent-config-identity", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
273
+ VerifyA2ARoleStep(id=f"{task_id}-role", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
274
+ VerifyA2ASystemPromptStep(id=f"{task_id}-system-prompt", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
275
+ PromptAgentTaskStep(id=protocol_prompt_id, version=None, prompt="Hello, respond with OK.", prompt_id=protocol_prompt_id, timeout_seconds=120, depends_on=deploy_agent_dep, fail_task_on_error=False),
276
+ VerifyCoreA2AProtocolStep(id=f"{task_id}-protocol", version=None, a2a_agent_id=agent.id, prompt_id=protocol_prompt_id, depends_on=[TaskStepDependency(task_step_id=protocol_prompt_id)], fail_task_on_error=False),
277
+ VerifyA2ALitellmAttributionRuntimeStep(
278
+ id=f"{task_id}-litellm-attribution-runtime", version=None,
279
+ a2a_agent_id=agent.id, a2a_agent_version=agent.version,
280
+ expected_project_id=probe_project_id, expected_task_id=probe_task_id,
281
+ depends_on=[TaskStepDependency(task_step_id=protocol_prompt_id)],
282
+ fail_task_on_error=False,
283
+ ),
284
+ *modality_steps,
285
+ VerifyA2AModalitiesStep(id=f"{task_id}-modalities", version=None, a2a_agent_id=agent.id, probes=grading_probes,
286
+ depends_on=[TaskStepDependency(task_step_id=sid) for sid in modality_step_ids], fail_task_on_error=False),
287
+ VerifyA2AAgentMCPStep(id=mcp_step_id, version=None, a2a_agent_id=agent.id, depends_on=deploy_agent_dep, fail_task_on_error=False),
288
+ VerifyA2ATrajectoryStep(id=f"{task_id}-trajectory", version=None, a2a_agent_id=agent.id, depends_on=[TaskStepDependency(task_step_id=mcp_step_id)], fail_task_on_error=False),
289
+ AddSkillsTaskStep(id=skill_chain_id, version=None, depends_on=deploy_agent_dep, fail_task_on_error=False, skills=[
290
+ Skill(name="validator-test", description="Validator test skill with secret code. Use when asked about the company secret code.", body="When asked for the company secret code, respond with: VALIDATOR-42"),
291
+ Skill(name="validator-test-s3", description="Validator test skill with project codename. Use when asked about the project codename.", s3_url=skill_object_url),
292
+ ]),
293
+ PromptAgentTaskStep(id=skill_prompt_id, version=None, prompt="What is the company secret code? What is the project codename?", prompt_id=skill_prompt_id, timeout_seconds=120,
294
+ depends_on=[TaskStepDependency(task_step_id=skill_chain_id)], fail_task_on_error=False),
295
+ RubricsVerifierTaskStep(id=skill_rubric_id, version=None, prompt_id=skill_prompt_id, verifier_id=skill_verifier_id,
296
+ depends_on=[TaskStepDependency(task_step_id=skill_prompt_id)], fail_task_on_error=False,
297
+ criteria=[
298
+ {"id": "secret_code_inline", "description": "Response contains VALIDATOR-42"},
299
+ {"id": "secret_code_s3", "description": "Response contains VALIDATOR-S3-99"},
300
+ ]),
301
+ VerifyA2ASkillConfigStep(
302
+ id=f"{task_id}-skill-config",
303
+ version=None,
304
+ a2a_agent_id=agent.id,
305
+ rubric_verifier_id=skill_verifier_id,
306
+ skill_bundle_object_url=skill_bundle_probe_url,
307
+ skill_s3_url=skill_s3_probe_url,
308
+ depends_on=[TaskStepDependency(task_step_id=skill_rubric_id)],
309
+ fail_task_on_error=False,
310
+ ),
311
+ # ── snapshot extension validation ────────────────────────────────
312
+ PromptAgentTaskStep(
313
+ id=snapshot_plant_prompt_id, version=None,
314
+ prompt=f"Remember exactly this token: {snapshot_token}. Just acknowledge with OK.",
315
+ prompt_id=snapshot_plant_prompt_id, timeout_seconds=120,
316
+ depends_on=deploy_agent_dep, fail_task_on_error=False,
317
+ ),
318
+ SnapshotAgentStateTaskStep(
319
+ id=snapshot_capture_id, version=None,
320
+ artifact_id=snapshot_artifact_id, prompt_id=snapshot_plant_prompt_id,
321
+ depends_on=[TaskStepDependency(task_step_id=snapshot_plant_prompt_id)],
322
+ fail_task_on_error=False,
323
+ ),
324
+ DeployAgentTaskStep(
325
+ id=snapshot_target_deploy_id, version=None,
326
+ env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
327
+ agent_name=snapshot_target_agent_name,
328
+ agent_snapshot_files_artifact_id=snapshot_artifact_id,
329
+ agent_snapshot_target_context_id=snapshot_target_context_id,
330
+ depends_on=[TaskStepDependency(task_step_id=snapshot_capture_id)],
331
+ fail_task_on_error=False,
332
+ ),
333
+ PromptAgentTaskStep(
334
+ id=snapshot_recall_prompt_id, version=None,
335
+ prompt="What was the exact token I told you to remember earlier? Output only the token, nothing else.",
336
+ prompt_id=snapshot_recall_prompt_id,
337
+ agent_name=snapshot_target_agent_name, context_id=snapshot_target_context_id,
338
+ timeout_seconds=120,
339
+ depends_on=[TaskStepDependency(task_step_id=snapshot_target_deploy_id)],
340
+ fail_task_on_error=False,
341
+ ),
342
+ RubricsVerifierTaskStep(
343
+ id=snapshot_recall_rubric_id, version=None,
344
+ prompt_id=snapshot_recall_prompt_id, verifier_id=snapshot_recall_verifier_id,
345
+ depends_on=[TaskStepDependency(task_step_id=snapshot_recall_prompt_id)],
346
+ fail_task_on_error=False,
347
+ criteria=[{"id": snapshot_recall_criterion_id, "description": f"Response contains the exact token {snapshot_token}"}],
348
+ ),
349
+ VerifyA2ASnapshotStep(
350
+ id=f"{task_id}-snapshot", version=None,
351
+ a2a_agent_id=agent.id,
352
+ rubric_verifier_id=snapshot_recall_verifier_id,
353
+ rubric_criterion_id=snapshot_recall_criterion_id,
354
+ depends_on=[TaskStepDependency(task_step_id=snapshot_recall_rubric_id)],
355
+ fail_task_on_error=False,
356
+ ),
357
+ # ── peer-agents extension validation ─────────────────────────────
358
+ DeployAgentTaskStep(
359
+ id=peer_target_deploy_id, version=None,
360
+ env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
361
+ agent_name=peer_target_agent_name,
362
+ depends_on=deploy_agent_dep,
363
+ fail_task_on_error=False,
364
+ ),
365
+ PeerAgentsTaskStep(
366
+ id=peer_setup_id, version=None,
367
+ peerings=[AgentPeers(source_agent_name=TaskStep.DEFAULT_AGENT_NAME, peer_agent_names=[peer_target_agent_name])],
368
+ depends_on=[TaskStepDependency(task_step_id=peer_target_deploy_id)],
369
+ fail_task_on_error=False,
370
+ ),
371
+ PromptAgentTaskStep(
372
+ id=peer_prompt_id, version=None,
373
+ agent_name=TaskStep.DEFAULT_AGENT_NAME,
374
+ prompt_id=peer_prompt_id, timeout_seconds=300,
375
+ prompt=(
376
+ f"Use the peer_send_message MCP tool to send the peer named "
377
+ f"'{peer_target_agent_name}' the exact prompt: 'Reply with only "
378
+ f"the single token {peer_token} and nothing else.' Then report "
379
+ f"the peer's full reply verbatim."
380
+ ),
381
+ depends_on=[TaskStepDependency(task_step_id=peer_setup_id)],
382
+ fail_task_on_error=False,
383
+ ),
384
+ VerifyA2APeerAgentsStep(
385
+ id=f"{task_id}-peer-agents", version=None,
386
+ a2a_agent_id=agent.id, a2a_agent_version=agent.version,
387
+ peer_prompt_id=peer_prompt_id, expected_token=peer_token,
388
+ depends_on=[TaskStepDependency(task_step_id=peer_prompt_id)],
389
+ fail_task_on_error=False,
390
+ ),
391
+ # ── agent-changelog validation (capture in DAG; apply driven
392
+ # by the validator post-run) ─────────────────────────────────────
393
+ DeployAgentTaskStep(
394
+ id=changelog_capture_deploy_id, version=None,
395
+ env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
396
+ agent_name=changelog_capture_agent_name, enable_agent_changelog=True,
397
+ depends_on=deploy_agent_dep, fail_task_on_error=False,
398
+ ),
399
+ PromptAgentTaskStep(
400
+ id=changelog_plant_prompt_id, version=None,
401
+ agent_name=changelog_capture_agent_name,
402
+ prompt=(
403
+ f"Run exactly one bash command and nothing else: write the exact text "
404
+ f"'{changelog_token}' (no trailing newline) to the file {changelog_marker_path}, "
405
+ f"e.g. printf %s '{changelog_token}' > {changelog_marker_path}"
406
+ ),
407
+ prompt_id=changelog_plant_prompt_id, timeout_seconds=180,
408
+ depends_on=[TaskStepDependency(task_step_id=changelog_capture_deploy_id)],
409
+ fail_task_on_error=False,
410
+ ),
411
+ DeployAgentTaskStep(
412
+ id=changelog_apply_deploy_id, version=None,
413
+ env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
414
+ agent_name=changelog_apply_agent_name,
415
+ depends_on=deploy_agent_dep, fail_task_on_error=False,
416
+ ),
417
+ ])
418
+ logger.info(f"Created A2A validation task: {task.id} version={task.version}")
419
+
420
+ # ── Execute task steps with progress callbacks ───────────────────────
421
+ log = on_progress or (lambda msg: None)
422
+ on_start = lambda i, total, step, ctx: log(f"Running step [{i+1}/{total}]: {step.type}...")
423
+ on_complete = lambda i, total, step, ctx, dur: log(f"Completed step [{i+1}/{total}]: {step.type} [{dur:.1f}s]")
424
+ initial_context = TaskStepContext()
425
+ if litellm_api_key:
426
+ initial_context.metadata["user_overrides"] = {"litellm_api_key": litellm_api_key}
427
+ # Plant LiteLLM attribution probe values so prompt_agent forwards them
428
+ # via /ext/agent-config — the runtime-attribution verify step reads them
429
+ # back from /ext/attribution-probe and asserts the agent's bridge
430
+ # actually wired them through to outbound LLM calls.
431
+ initial_context.metadata["project_id"] = probe_project_id
432
+ initial_context.metadata["task_id"] = probe_task_id
433
+ num_deploy_steps = 2
434
+ run_kwargs = {"on_step_start": on_start, "on_step_complete": on_complete}
435
+
436
+ # Phase 1: deploy steps (must succeed)
437
+ context = await task.run(start_step=0, end_step=num_deploy_steps, context=initial_context, **run_kwargs)
438
+
439
+ # Phase 2: validation DAG — every step opts into fail_task_on_error=False
440
+ # at construction so independent chains keep running on per-step failures.
441
+ context = await task.run(start_step=num_deploy_steps, context=context, **run_kwargs)
442
+
443
+ # agent-changelog: the validator drives apply directly — it discovers the
444
+ # captured prefix from context, applies it onto the fresh apply agent,
445
+ # and checks the marker file was reconstructed.
446
+ await A2AAgentValidator._validate_agent_changelog(
447
+ agent, context,
448
+ capture_agent_name=changelog_capture_agent_name,
449
+ apply_agent_name=changelog_apply_agent_name,
450
+ marker_path=changelog_marker_path, token=changelog_token,
451
+ on_progress=on_progress,
452
+ )
453
+
454
+ # install/v1: gated on the LIVE deployed card (not the stored one), so it
455
+ # runs identically regardless of whether this agent was validated before.
456
+ await A2AAgentValidator._validate_install(
457
+ agent, context, task_id=task_id, on_progress=on_progress,
458
+ )
459
+
460
+ await A2AAgentValidator._cleanup_sandboxes(context)
461
+ return context.metadata.get("verifications", {})
462
+
463
+ @staticmethod
464
+ async def _validate_install(
465
+ agent: "A2AAgent",
466
+ context,
467
+ task_id: str,
468
+ on_progress: Optional[Callable[[str], None]] = None,
469
+ ) -> None:
470
+ """Validate the install/v1 extension, gated on the LIVE deployed card.
471
+
472
+ Runs after the main DAG (mirrors _validate_agent_changelog). The
473
+ install-support decision reads the freshly-deployed agent's card off
474
+ context.deployed_agents — NOT the stored agent.metadata["agent_card"] —
475
+ so the result is identical whether or not the agent was validated before
476
+ (fixes the --skip-validation first-run "upstream chain failure"). The
477
+ sub-chain runs on the MAIN context so its host sandbox is reaped by
478
+ _cleanup_sandboxes and its result lands in the returned verifications dict.
479
+ """
480
+ from agent_env.a2a_agent import A2AAgent
481
+ from agent_env.task import Task
482
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
483
+ from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_install import VerifyA2AInstallStep
484
+ from agent_env.task_step.task_steps.deploy_sandbox import DeploySandboxTaskStep
485
+ from agent_env.task_step.task_steps.install_agent import InstallAgentTaskStep
486
+ from agent_env.task_step.task_steps.prompt_agent import PromptAgentTaskStep
487
+ from agent_env.task_step.task_steps.run_docker_container import RunDockerContainerTaskStep
488
+ from agent_env.task_step.task_steps.verifiers.rubrics_verifier import RubricsVerifierTaskStep
489
+
490
+ log = on_progress or (lambda _msg: None)
491
+
492
+ def record(result: dict) -> None:
493
+ try:
494
+ fresh = A2AAgent.get(agent.id)
495
+ validated = dict(fresh.metadata.get("validated_a2a_extensions", {}))
496
+ validated[A2AAgent.EXT_INSTALL] = result
497
+ fresh.update_metadata({**fresh.metadata, "validated_a2a_extensions": validated})
498
+ except Exception as e: # noqa: BLE001
499
+ logger.warning(f"install/v1: failed to persist validated_a2a_extensions: {e}")
500
+ context.metadata.setdefault("verifications", {})["a2a_install"] = result
501
+
502
+ main = next((a for a in context.deployed_agents if a.agent_name == TaskStep.DEFAULT_AGENT_NAME), None)
503
+ # deploy_agent (Phase 1, fail_task_on_error=True) normally guarantees a
504
+ # fetched card, so a missing card here is an anomaly (e.g. a transient
505
+ # /.well-known/agent.json failure), NOT a genuine "not advertised". Log it
506
+ # so the failure mode is observable, and record a distinct reason rather
507
+ # than silently masquerading as absence of the extension.
508
+ card_unavailable = main is None or main.a2a_card is None
509
+ if card_unavailable:
510
+ logger.warning(
511
+ "install/v1: no live a2a_card for deployed agent %r (main_present=%s) — "
512
+ "possible transient card-fetch failure; recording install/v1 as unavailable",
513
+ TaskStep.DEFAULT_AGENT_NAME, main is not None,
514
+ )
515
+ live_card = (main.a2a_card if main else None) or {}
516
+ if A2AAgent.find_extension(live_card, A2AAgent.EXT_INSTALL) is None:
517
+ reason = (
518
+ "live agent card unavailable — deployed agent card was None"
519
+ if card_unavailable
520
+ else "install/v1 not advertised on live agent card"
521
+ )
522
+ log(f"install/v1 skipped: {reason}")
523
+ record({
524
+ "supported": False, "advertised": False,
525
+ "installed_successfully": False, "responded_to_prompt": False,
526
+ "skipped_reason": reason,
527
+ })
528
+ return
529
+
530
+ sandbox_name = "install-test-host"
531
+ container_name = "install-test-container"
532
+ install_agent_name = "install-test-agent"
533
+ prompt_id = f"{task_id}-install-test-prompt"
534
+ verifier_id = f"{task_id}-install-test-rubric"
535
+ deploy_sandbox_id = f"{task_id}-install-test-deploy-sandbox"
536
+ run_container_id = f"{task_id}-install-test-run-container"
537
+ install_id = f"{task_id}-install-test-install"
538
+ rubric_step_id = f"{task_id}-install-test-rubric-step"
539
+ validator_id = f"{task_id}-install-validator"
540
+
541
+ try:
542
+ install_test_image = await asyncio.to_thread(A2AAgentValidator._upload_install_test_image_fixture, agent)
543
+ install_steps = [
544
+ DeploySandboxTaskStep(
545
+ id=deploy_sandbox_id, version=None,
546
+ sandbox_name=sandbox_name, sandbox_mode="vm",
547
+ cpu=2, memory_mb=4096, disk_size_gb=10,
548
+ exposed_ports=[8000], ttl_seconds=1800,
549
+ fail_task_on_error=False,
550
+ ),
551
+ RunDockerContainerTaskStep(
552
+ id=run_container_id, version=None,
553
+ sandbox_name=sandbox_name,
554
+ docker_context_artifact_id=install_test_image.id,
555
+ docker_context_artifact_version=install_test_image.version,
556
+ container_name=container_name,
557
+ image_tag=f"{container_name}:latest",
558
+ ports=[8000],
559
+ command_override="sleep infinity",
560
+ depends_on=[TaskStepDependency(task_step_id=deploy_sandbox_id)],
561
+ fail_task_on_error=False,
562
+ ),
563
+ InstallAgentTaskStep(
564
+ id=install_id, version=None,
565
+ sandbox_name=sandbox_name,
566
+ container_name=container_name,
567
+ a2a_agent_id=agent.id, a2a_agent_version=agent.version,
568
+ agent_name=install_agent_name,
569
+ depends_on=[TaskStepDependency(task_step_id=run_container_id)],
570
+ fail_task_on_error=False,
571
+ ),
572
+ PromptAgentTaskStep(
573
+ id=prompt_id, version=None,
574
+ prompt="Respond with OK.",
575
+ prompt_id=prompt_id,
576
+ agent_name=install_agent_name,
577
+ timeout_seconds=120,
578
+ depends_on=[TaskStepDependency(task_step_id=install_id)],
579
+ fail_task_on_error=False,
580
+ ),
581
+ RubricsVerifierTaskStep(
582
+ id=rubric_step_id, version=None,
583
+ prompt_id=prompt_id,
584
+ verifier_id=verifier_id,
585
+ # Trivial "response is non-empty" check — use the direct-LLM
586
+ # judge instead of auto-deploying an ephemeral judge agent.
587
+ use_agent_judge=False,
588
+ criteria=[
589
+ {"id": "responded", "description": "Response is non-empty (the installed agent actually responded)"},
590
+ ],
591
+ depends_on=[TaskStepDependency(task_step_id=prompt_id)],
592
+ fail_task_on_error=False,
593
+ ),
594
+ VerifyA2AInstallStep(
595
+ id=validator_id, version=None,
596
+ a2a_agent_id=agent.id, a2a_agent_version=agent.version,
597
+ agent_name=install_agent_name,
598
+ rubric_verifier_id=verifier_id,
599
+ depends_on=[TaskStepDependency(task_step_id=rubric_step_id)],
600
+ fail_task_on_error=False,
601
+ ),
602
+ ]
603
+ on_start = lambda i, total, step, ctx: log(f"Running install step [{i+1}/{total}]: {step.type}...")
604
+ on_complete = lambda i, total, step, ctx, dur: log(f"Completed install step [{i+1}/{total}]: {step.type} [{dur:.1f}s]")
605
+ sub_task = Task(id=f"{task_id}-install", version=None, steps=install_steps)
606
+ # Run on the MAIN context: the install host lands in context.deployed_agents
607
+ # (cleaned up by _cleanup_sandboxes) and VerifyA2AInstallStep writes the
608
+ # result into context.metadata["verifications"]["a2a_install"] itself.
609
+ await sub_task.run(context=context, on_step_start=on_start, on_step_complete=on_complete)
610
+ except Exception as e: # noqa: BLE001
611
+ logger.warning(f"install/v1 validation raised: {e}")
612
+ record({
613
+ "supported": False, "advertised": True,
614
+ "installed_successfully": False, "responded_to_prompt": False,
615
+ "error": f"install validation raised: {e}",
616
+ })
617
+
618
+ @staticmethod
619
+ async def _validate_agent_changelog(
620
+ agent: "A2AAgent",
621
+ context,
622
+ capture_agent_name: str,
623
+ apply_agent_name: str,
624
+ marker_path: str,
625
+ token: str,
626
+ on_progress: Optional[Callable[[str], None]] = None,
627
+ ) -> None:
628
+ """Drive the agent-changelog enable→apply round-trip directly (no task step).
629
+
630
+ Discovers the captured changelog prefix from context.metadata['agent_changelog']
631
+ (recorded by the capture agent's deploy), PUTs apply onto the fresh apply
632
+ agent, then deterministically checks the marker file was reconstructed by
633
+ exec-ing into the apply agent's sandbox. Records the outcome on the agent's
634
+ validated_a2a_extensions and in context.metadata['verifications']."""
635
+ from agent_env.a2a_agent import A2AAgent
636
+ from agent_env.providers.sandbox_providers.sandbox_provider import (
637
+ SANDBOX_MODE_VM, build_sandbox_provider, get_agent_sandbox_provider,
638
+ )
639
+
640
+ log = on_progress or (lambda _msg: None)
641
+
642
+ def record(*, supported, advertised, save_ok, apply_ok, roundtrip_ok, note=""):
643
+ try:
644
+ fresh = A2AAgent.get(agent.id)
645
+ validated = fresh.metadata.get("validated_a2a_extensions", {})
646
+ snap = validated.setdefault(A2AAgent.EXT_SNAPSHOT, {})
647
+ methods = snap.setdefault("methods", {})
648
+ methods[A2AAgent.SNAPSHOT_METHOD_ENABLE_CHANGELOG] = {"supported": save_ok}
649
+ methods[A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG] = {"supported": apply_ok and roundtrip_ok}
650
+ snap["changelog_roundtrip"] = roundtrip_ok
651
+ fresh.update_metadata({**fresh.metadata, "validated_a2a_extensions": validated})
652
+ except Exception as e: # noqa: BLE001
653
+ logger.warning(f"agent-changelog: failed to persist validated_a2a_extensions: {e}")
654
+ context.metadata.setdefault("verifications", {})["a2a_agent_changelog"] = {
655
+ "methods_advertised": advertised, "save": save_ok,
656
+ "apply": apply_ok, "roundtrip": roundtrip_ok, "note": note,
657
+ }
658
+ logger.info(
659
+ f"agent-changelog validation: advertised={advertised} save={save_ok} "
660
+ f"apply={apply_ok} roundtrip={roundtrip_ok} {note}"
661
+ )
662
+
663
+ capture = next(
664
+ (e for e in (context.metadata.get("agent_changelog") or [])
665
+ if e.get("agent_name") == capture_agent_name), None)
666
+ capture_source = capture.get("object_url") if capture else None
667
+ apply_agent = next(
668
+ (d for d in context.deployed_agents if d.agent_name == apply_agent_name), None)
669
+ advertised = apply_agent is not None and A2AAgent.extension_method(
670
+ apply_agent.a2a_card or {}, A2AAgent.EXT_SNAPSHOT,
671
+ A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG) is not None
672
+ save_ok = capture_source is not None
673
+ if not save_ok or apply_agent is None or not advertised:
674
+ record(supported=False, advertised=advertised, save_ok=save_ok,
675
+ apply_ok=False, roundtrip_ok=False,
676
+ note="capture prefix or apply agent missing / extension not advertised")
677
+ return
678
+
679
+ # Apply the captured changelog onto the fresh apply agent.
680
+ method, path = A2AAgent.operation(
681
+ A2AAgent.find_extension(apply_agent.a2a_card, A2AAgent.EXT_SNAPSHOT),
682
+ A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG)
683
+ a2a_url = apply_agent.a2a_url or apply_agent.api_url
684
+ try:
685
+ call = await asyncio.to_thread(
686
+ changelog_apply_call,
687
+ method,
688
+ get_config().get_object_store(),
689
+ agent_name=apply_agent_name,
690
+ source_url=capture_source,
691
+ portable=capture.get("transfer_mode") == "objects",
692
+ )
693
+ except RuntimeError as e:
694
+ record(supported=False, advertised=advertised, save_ok=save_ok,
695
+ apply_ok=False, roundtrip_ok=False, note=str(e))
696
+ return
697
+ except Exception as e: # noqa: BLE001
698
+ record(supported=False, advertised=advertised, save_ok=save_ok,
699
+ apply_ok=False, roundtrip_ok=False, note=f"apply request failed: {e}")
700
+ return
701
+ try:
702
+ log("Applying agent-changelog onto apply agent...")
703
+ answer = await invoke_transfer(
704
+ a2a_url + path,
705
+ call,
706
+ verb="PUT",
707
+ operation="changelog apply",
708
+ timeout=TRANSFER_TIMEOUT_SECONDS,
709
+ response_model=ObjectChangelogApplyResponse,
710
+ )
711
+ if call.mode == "objects":
712
+ check_changelog_applied(answer, call, agent_name=apply_agent_name)
713
+ except Exception as e: # noqa: BLE001
714
+ record(supported=False, advertised=advertised, save_ok=save_ok,
715
+ apply_ok=False, roundtrip_ok=False, note=f"apply request failed: {e}")
716
+ return
717
+
718
+ # Deterministically verify the marker file was reconstructed.
719
+ roundtrip_ok = False
720
+ provider = None
721
+ try:
722
+ provider = (build_sandbox_provider(apply_agent.sandbox_type)
723
+ if apply_agent.sandbox_type else get_agent_sandbox_provider())
724
+ sandbox = await provider.get_sandbox(apply_agent.sandbox_id)
725
+ if sandbox.mode == SANDBOX_MODE_VM:
726
+ container = await A2AAgentValidator._discover_agent_container(sandbox)
727
+ args = ("sudo", "docker", "exec", container, "cat", marker_path)
728
+ else:
729
+ args = ("cat", marker_path)
730
+ exit_code, stdout, _ = await sandbox.exec_with_output(*args)
731
+ roundtrip_ok = exit_code == 0 and token in (stdout or "")
732
+ except Exception as e: # noqa: BLE001
733
+ record(supported=False, advertised=advertised, save_ok=save_ok,
734
+ apply_ok=True, roundtrip_ok=False, note=f"marker verification failed: {e}")
735
+ return
736
+ finally:
737
+ if provider is not None:
738
+ try:
739
+ await provider.close()
740
+ except Exception: # noqa: BLE001
741
+ pass
742
+
743
+ record(supported=roundtrip_ok, advertised=advertised, save_ok=save_ok,
744
+ apply_ok=True, roundtrip_ok=roundtrip_ok)
745
+
746
+ @staticmethod
747
+ async def _discover_agent_container(sandbox) -> str:
748
+ """Find the agent container on a VM sandbox (mirrors collect_artifacts /
749
+ verify_sandbox): prefer 'agent-api', else the first 'a2a-agent-*'."""
750
+ exit_code, stdout, stderr = await sandbox.exec_with_output(
751
+ "sudo", "docker", "ps", "--format", "{{.Names}}")
752
+ if exit_code != 0:
753
+ raise RuntimeError(f"docker ps failed: {stderr[:200]}")
754
+ running = [n.strip() for n in stdout.splitlines() if n.strip()]
755
+ if sandbox.container_name in running:
756
+ return sandbox.container_name
757
+ fallback = [n for n in running if n.startswith("a2a-agent-")]
758
+ if not fallback:
759
+ raise RuntimeError(f"no agent container found; running: {running}")
760
+ return fallback[0]
761
+
762
+ @staticmethod
763
+ def _upload_install_test_image_fixture(agent: "A2AAgent"):
764
+ """Upload a minimal Dockerfile as a FileArtifactUniverse so the install
765
+ validator chain has a base task image to layer the agent onto.
766
+
767
+ The Dockerfile is intentionally minimal: `FROM ubuntu:24.04`, no CMD.
768
+ The validator pairs it with `command_override="sleep infinity"` on
769
+ RunDockerContainerTaskStep so the bare container stays alive long
770
+ enough for the install to finish.
771
+ """
772
+ from pathlib import Path
773
+ import os
774
+ import tempfile
775
+ import time
776
+
777
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
778
+
779
+ ts = int(time.time())
780
+ universe_id = f"validate-install-image-{agent.id}-v{agent.version}-{ts}"
781
+ config = get_config()
782
+ s3_url = config.get_object_store().object_url(f"{config.get_artifact_key_prefix()}a2a_validator/install_test_image/{ts}/")
783
+
784
+ with tempfile.NamedTemporaryFile("w", suffix=".Dockerfile", delete=False) as f:
785
+ f.write("FROM ubuntu:24.04\n")
786
+ tmp_path = f.name
787
+ try:
788
+ universe = FileArtifactUniverse.put_bundled(
789
+ id=universe_id,
790
+ files={"Dockerfile": Path(tmp_path)},
791
+ s3_url=s3_url,
792
+ )
793
+ logger.info(f"Uploaded install-test image universe '{universe.id}' v{universe.version} to {s3_url}")
794
+ return universe
795
+ finally:
796
+ try:
797
+ os.unlink(tmp_path)
798
+ except OSError:
799
+ pass
800
+
801
+ @staticmethod
802
+ def _upload_skill_fixture(
803
+ agent: "A2AAgent", *, name: str, description: str, body: str
804
+ ) -> str:
805
+ from agent_env.task_step.task_steps.add_skills import Skill
806
+
807
+ config = get_config()
808
+ store = config.get_object_store()
809
+ prefix = (
810
+ f"{config.get_artifact_key_prefix()}a2a_validator/validator_skill/{agent.id}-v{agent.version}/{name}/"
811
+ )
812
+ skill = Skill(
813
+ name=name,
814
+ description=description,
815
+ body=body,
816
+ )
817
+ store.put(f"{prefix}SKILL.md", skill.to_skill_md().encode(), content_type="text/markdown", allow_overwrite=True)
818
+ object_url = store.object_url(prefix)
819
+ logger.info(f"Uploaded test skill to {object_url}")
820
+ return object_url
821
+
822
+ @staticmethod
823
+ def _upload_probe_fixtures(agent: "A2AAgent", skill_object_url: str) -> _UploadedFixtures:
824
+ """Upload `red.png` and `clip.mp4` for the URI probes.
825
+
826
+ The PNG bytes are also sent inline via the existing PNG probe — comparing
827
+ the two outcomes verifies both FilePart delivery modes work for this
828
+ wrapper. The MP4 is too big for practical inline use and the URI path is
829
+ the point.
830
+ """
831
+
832
+ config = get_config()
833
+ store = config.get_object_store()
834
+ prefix = f"{config.get_artifact_key_prefix()}a2a_validator/probe_fixtures/{agent.id}-v{agent.version}/"
835
+
836
+ png_object_uri = store.put(f"{prefix}red.png", base64.b64decode(IMAGE_PROBE_PNG_B64), content_type="image/png", allow_overwrite=True)
837
+ png_signed_url = store.signed_get_url(png_object_uri)
838
+ if png_signed_url is None:
839
+ raise RuntimeError("A2A validation requires a signable object store for the presigned-URI probe.")
840
+ logger.info(f"Uploaded probe fixture to {png_object_uri}")
841
+
842
+ mp4_object_uri = store.put(f"{prefix}clip.mp4", base64.b64decode(VIDEO_PROBE_MP4_B64), content_type="video/mp4", allow_overwrite=True)
843
+ logger.info(f"Uploaded video probe fixture to {mp4_object_uri}")
844
+
845
+ return _UploadedFixtures(
846
+ skill_object_url=skill_object_url,
847
+ png_object_uri=png_object_uri,
848
+ png_signed_url=png_signed_url,
849
+ mp4_object_uri=mp4_object_uri,
850
+ )
851
+
852
+ @staticmethod
853
+ def _build_modality_steps(task_id: str, fixtures: _UploadedFixtures, deploy_agent_id: str):
854
+ """Build modality probe `PromptAgentTaskStep`s and the matching grading list.
855
+
856
+ All 11 prompts depend only on deploy-agent so they fan out in parallel
857
+ once the agent is deployed. Adding a new probe is one new entry in
858
+ MODALITY_PROBES, nothing else.
859
+ """
860
+ from agent_env.task_step import PromptAgentTaskStep
861
+ from agent_env.task_step.task_step import TaskStepDependency
862
+ prompt_steps: list[PromptAgentTaskStep] = []
863
+ grading_probes: list[dict] = []
864
+ for probe in MODALITY_PROBES:
865
+ prompt_id = f"{task_id}-{probe.prompt_id_suffix}-prompt"
866
+ parts = probe.parts(fixtures) if callable(probe.parts) else probe.parts
867
+ prompt_steps.append(PromptAgentTaskStep(
868
+ id=f"{task_id}-{probe.prompt_id_suffix}-prompt-step",
869
+ version=None, parts=parts, prompt_id=prompt_id, timeout_seconds=probe.timeout_s,
870
+ depends_on=[TaskStepDependency(task_step_id=deploy_agent_id)],
871
+ fail_task_on_error=False,
872
+ ))
873
+ grading_probes.append({
874
+ "modality": probe.modality,
875
+ "prompt_id": prompt_id,
876
+ "expected": probe.expected,
877
+ })
878
+ return prompt_steps, grading_probes
879
+
880
+ @staticmethod
881
+ async def _cleanup_sandboxes(context) -> None:
882
+ from agent_env.env.env import DeployedSandboxEnv
883
+ from agent_env.providers import build_sandbox_provider, get_agent_sandbox_provider, get_env_sandbox_provider
884
+ for deployed_env in context.deployed_envs:
885
+ if not isinstance(deployed_env, DeployedSandboxEnv): # an env outside our sandboxes owns its own lifetime
886
+ continue
887
+ try:
888
+ provider = build_sandbox_provider(deployed_env.sandbox_type) if deployed_env.sandbox_type else get_env_sandbox_provider()
889
+ sandbox = await provider.get_sandbox(deployed_env.sandbox_id)
890
+ await sandbox.terminate()
891
+ except Exception as e:
892
+ logger.warning(f"Failed to clean up env sandbox {deployed_env.sandbox_id}: {e}")
893
+ for deployed_agent in context.deployed_agents:
894
+ if not deployed_agent.sandbox_id:
895
+ continue
896
+ try:
897
+ provider = build_sandbox_provider(deployed_agent.sandbox_type) if deployed_agent.sandbox_type else get_agent_sandbox_provider()
898
+ sandbox = await provider.get_sandbox(deployed_agent.sandbox_id)
899
+ await sandbox.terminate()
900
+ except Exception as e:
901
+ logger.warning(f"Failed to clean up agent sandbox {deployed_agent.sandbox_id}: {e}")