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,173 @@
1
+ """Verify A2A agent actually emits LiteLLM attribution on outbound LLM calls.
2
+
3
+ Optional behavioral counterpart to the static
4
+ `VerifyA2ALitellmAttributionStep`. When a runtime advertises its private probe,
5
+ this step asks whether it actually forwards project_id/task_id to the LLM call.
6
+ The probe is not part of the general AgentEnv SDK contract.
7
+
8
+ How it works:
9
+ 1. The validator plants unique probe values in `initial_context.metadata`
10
+ before any prompt fires.
11
+ 2. The standard `prompt_agent` flow POSTs those values via
12
+ `/ext/agent-config` and sends a prompt — the agent processes it,
13
+ makes an outbound LLM call, and records the attribution it claims
14
+ to have emitted in module-level state.
15
+ 3. This step queries the agent's `/ext/attribution-probe` extension to
16
+ read back that recorded state.
17
+ 4. Passes when the recorded values match the planted probe values.
18
+
19
+ Catches:
20
+ - Bridge ignores project_id/task_id from agent-config
21
+ - Bridge sets the wrong env var / header names
22
+ - Agent advertises support but never wires it through
23
+
24
+ Does NOT catch:
25
+ - CLI eats the env var and never emits the actual HTTP header (probe
26
+ trusts the bridge's claim about what it sent)
27
+ - Proxy strips the header on receive
28
+
29
+ For full end-to-end coverage of the latter two, a spend-log probe would
30
+ be needed (requires proxy admin access).
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ from typing import ClassVar, Optional
37
+
38
+ import httpx
39
+
40
+ from agent_env.task_step.context import TaskStepContext
41
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ EXT_ATTRIBUTION_PROBE_URI = "urn:agentenv:attribution-probe/v1"
46
+ EXT_ATTRIBUTION_PROBE_PATH = "/ext/attribution-probe"
47
+
48
+
49
+ class VerifyA2ALitellmAttributionRuntimeStep(TaskStep):
50
+ type: ClassVar[str] = "verify_a2a_litellm_attribution_runtime"
51
+
52
+ def __init__(
53
+ self,
54
+ id: str,
55
+ version: Optional[int],
56
+ a2a_agent_id: str,
57
+ expected_project_id: str,
58
+ expected_task_id: str,
59
+ a2a_agent_version: Optional[int] = None,
60
+ depends_on: Optional[list[TaskStepDependency]] = None,
61
+ fail_task_on_error: bool = True,
62
+ ):
63
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
64
+ self.a2a_agent_id = a2a_agent_id
65
+ self.a2a_agent_version = a2a_agent_version
66
+ self.expected_project_id = expected_project_id
67
+ self.expected_task_id = expected_task_id
68
+
69
+ def to_dict(self) -> dict:
70
+ base = super().to_dict()
71
+ base["a2a_agent_id"] = self.a2a_agent_id
72
+ base["a2a_agent_version"] = self.a2a_agent_version
73
+ base["expected_project_id"] = self.expected_project_id
74
+ base["expected_task_id"] = self.expected_task_id
75
+ return base
76
+
77
+ @classmethod
78
+ def from_dict(cls, data: dict) -> "VerifyA2ALitellmAttributionRuntimeStep":
79
+ return cls(
80
+ **cls._base_from_dict(data),
81
+ a2a_agent_id=data["a2a_agent_id"],
82
+ a2a_agent_version=data.get("a2a_agent_version"),
83
+ expected_project_id=data["expected_project_id"],
84
+ expected_task_id=data["expected_task_id"],
85
+ )
86
+
87
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
88
+ from agent_env.a2a_agent import A2AAgent
89
+
90
+ deployed = next((a for a in context.deployed_agents), None)
91
+ if deployed is None:
92
+ raise RuntimeError("No deployed agent found in context")
93
+
94
+ agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
95
+
96
+ # This is a runtime-specific diagnostic, not a general SDK capability.
97
+ # Its absence is therefore not a conformance failure.
98
+ probe_ext = A2AAgent.find_extension(deployed.a2a_card or {}, EXT_ATTRIBUTION_PROBE_URI)
99
+ if probe_ext is None:
100
+ logger.info(
101
+ "Agent '%s' does not advertise the attribution-probe extension "
102
+ "(%s) — skipping optional runtime attribution verification.",
103
+ self.a2a_agent_id, EXT_ATTRIBUTION_PROBE_URI,
104
+ )
105
+ validated = {
106
+ "passed": True,
107
+ "skipped": True,
108
+ "extension_present": False,
109
+ "reason": "optional runtime attribution probe not advertised",
110
+ "expected_project_id": self.expected_project_id,
111
+ "expected_task_id": self.expected_task_id,
112
+ }
113
+ agent.update_metadata({**agent.metadata, "validated_litellm_attribution_runtime": validated})
114
+ context.metadata.setdefault("verifications", {})["a2a_litellm_attribution_runtime"] = validated
115
+ return context
116
+
117
+ endpoint = (probe_ext.get("params") or {}).get("endpoint", EXT_ATTRIBUTION_PROBE_PATH)
118
+ url = (deployed.a2a_url or deployed.api_url) + endpoint
119
+ logger.info("Querying attribution probe at %s", url)
120
+
121
+ try:
122
+ async with httpx.AsyncClient(timeout=10.0) as client:
123
+ resp = await client.post(url, json={})
124
+ resp.raise_for_status()
125
+ probe_data = resp.json()
126
+ except Exception as e:
127
+ logger.warning("attribution-probe request failed: %s", e)
128
+ validated = {
129
+ "passed": False,
130
+ "extension_present": True,
131
+ "reason": f"probe request failed: {e}",
132
+ "expected_project_id": self.expected_project_id,
133
+ "expected_task_id": self.expected_task_id,
134
+ }
135
+ agent.update_metadata({**agent.metadata, "validated_litellm_attribution_runtime": validated})
136
+ context.metadata.setdefault("verifications", {})["a2a_litellm_attribution_runtime"] = validated
137
+ return context
138
+
139
+ last_seen = probe_data.get("last_seen_attribution") or {}
140
+ observed_project_id = last_seen.get("project_id")
141
+ observed_task_id = last_seen.get("task_id")
142
+
143
+ project_matches = observed_project_id == self.expected_project_id
144
+ task_matches = observed_task_id == self.expected_task_id
145
+ passed = project_matches and task_matches
146
+
147
+ if passed:
148
+ logger.info(
149
+ "Runtime attribution verified for '%s': observed projectId=%s taskId=%s",
150
+ self.a2a_agent_id, observed_project_id, observed_task_id,
151
+ )
152
+ else:
153
+ logger.warning(
154
+ "Runtime attribution mismatch for '%s': expected projectId=%s taskId=%s, "
155
+ "observed projectId=%s taskId=%s",
156
+ self.a2a_agent_id, self.expected_project_id, self.expected_task_id,
157
+ observed_project_id, observed_task_id,
158
+ )
159
+
160
+ validated = {
161
+ "passed": passed,
162
+ "extension_present": True,
163
+ "expected_project_id": self.expected_project_id,
164
+ "expected_task_id": self.expected_task_id,
165
+ "observed_project_id": observed_project_id,
166
+ "observed_task_id": observed_task_id,
167
+ "project_matches": project_matches,
168
+ "task_matches": task_matches,
169
+ "last_seen_at_utc": probe_data.get("last_seen_at_utc"),
170
+ }
171
+ agent.update_metadata({**agent.metadata, "validated_litellm_attribution_runtime": validated})
172
+ context.metadata.setdefault("verifications", {})["a2a_litellm_attribution_runtime"] = validated
173
+ return context
@@ -0,0 +1,270 @@
1
+ """Verify which input modalities an A2A agent functionally supports.
2
+
3
+ Covers `text`, `image/{png,jpeg,gif}`, `audio/{wav,mpeg,mp4,ogg}`,
4
+ `application/pdf`, and `video/mp4`.
5
+ The companion `PromptAgentTaskStep` probes (configured in
6
+ `A2AAgent.validate()`) actually send the messages via A2A; this step purely
7
+ grades the resulting `PromptResponse`s.
8
+
9
+ Grading is a deterministic case-insensitive substring match against a known
10
+ ground-truth string per probe. The outcome for each modality is one of:
11
+ - `passed`: response contains the expected string.
12
+ - `no_ingestion_evidence`: response succeeded but didn't reference the asset.
13
+ - `protocol_reject`: response was appended with `error_type` (wrapper accepted
14
+ the message but the agent task ended in `failed`).
15
+ - `probe_step_did_not_run`: no matching response — the upstream
16
+ `PromptAgentTaskStep` crashed before `context.prompt_responses.append(...)`.
17
+
18
+ The gemini wrapper currently passes all 12 modality probes (text, image
19
+ {png,jpeg,gif}, audio {wav,mpeg,mp4,ogg}, PDF, video/mp4, plus image/png
20
+ delivered via `FileWithUri` over `s3://` and presigned `https://`). New
21
+ A2A wrappers (claude-code, codex, grok) inherit this same probe set and
22
+ will report which subset they actually ingest.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import base64
28
+ import functools
29
+ import logging
30
+ from pathlib import Path
31
+ from typing import ClassVar, Optional
32
+
33
+ from agent_env.task_step.context import TaskStepContext
34
+ from agent_env.entity_refs import EntityRef
35
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ _FIXTURES_DIR = Path(__file__).parent / "fixtures"
41
+
42
+
43
+ @functools.lru_cache(maxsize=None)
44
+ def _b64(name: str) -> str:
45
+ """Load a binary fixture and return its base64-encoded form.
46
+
47
+ Fixtures live as native binary files in `fixtures/` so they can be
48
+ inspected (e.g. `afplay clip.wav`, `qlmanage -p document.pdf`) and
49
+ replaced in-place without re-chunking base64 into Python source.
50
+ """
51
+ return base64.b64encode((_FIXTURES_DIR / name).read_bytes()).decode("ascii")
52
+
53
+
54
+ # 8×8 solid red squares in three formats — unambiguously red so the probe's
55
+ # deterministic "red" substring match holds. See `fixtures/README.md`.
56
+ IMAGE_PROBE_PNG_B64 = _b64("red.png")
57
+ IMAGE_PROBE_JPEG_B64 = _b64("red.jpg")
58
+ IMAGE_PROBE_GIF_B64 = _b64("red.gif")
59
+
60
+ # Per-format spoken "Say the word: <X>" clips with distinct target words —
61
+ # WAV/banana, MP3/telephone, M4A/bicycle, OGG/rainbow. Distinct targets force
62
+ # the model to actually decode each clip. See `fixtures/README.md` for the
63
+ # encoding pipeline, voice/volume/padding rationale, and regen commands.
64
+ AUDIO_PROBE_WAV_B64 = _b64("clip.wav")
65
+ AUDIO_PROBE_MP3_B64 = _b64("clip.mp3")
66
+ AUDIO_PROBE_M4A_B64 = _b64("clip.m4a")
67
+ AUDIO_PROBE_OGG_B64 = _b64("clip.ogg")
68
+
69
+ TEXT_PROBE_PROMPT = "Reply with exactly the word PINEAPPLE and nothing else."
70
+ TEXT_PROBE_EXPECTED = "PINEAPPLE"
71
+
72
+ IMAGE_PROBE_PROMPT = (
73
+ "What is the dominant color of the image attached? Answer in one word."
74
+ )
75
+ IMAGE_PROBE_EXPECTED = "red"
76
+
77
+ AUDIO_PROBE_PROMPT = (
78
+ "An audio clip is attached. Follow the spoken instruction in it and "
79
+ "respond with only the requested word."
80
+ )
81
+ AUDIO_WAV_EXPECTED = "banana"
82
+ AUDIO_MP3_EXPECTED = "telephone"
83
+ AUDIO_M4A_EXPECTED = "bicycle"
84
+ AUDIO_OGG_EXPECTED = "rainbow"
85
+
86
+ # 1.1KB single-page PDF with "ELEPHANT" inside an instructional paragraph
87
+ # (bare word alone trips letter-truncation edge cases). See `fixtures/README.md`.
88
+ PDF_PROBE_PDF_B64 = _b64("document.pdf")
89
+ PDF_PROBE_PROMPT = (
90
+ "A PDF document is attached. Read it and reply with only the target "
91
+ "word it specifies."
92
+ )
93
+ PDF_PROBE_EXPECTED = "elephant"
94
+
95
+ # 8.7KB h.264 mp4 (3s @ 480×360, 15fps, no audio) showing "GIRAFFE" on a
96
+ # static frame with on-screen instructional context. Delivered via
97
+ # FileWithUri (s3://) at validator runtime. See `fixtures/README.md`.
98
+ VIDEO_PROBE_MP4_B64 = _b64("clip.mp4")
99
+ VIDEO_PROBE_PROMPT = (
100
+ "A short video clip is attached. Read the target word displayed in "
101
+ "the video frames and reply with only that word."
102
+ )
103
+ VIDEO_PROBE_EXPECTED = "giraffe"
104
+
105
+ TEXT_PROBE_PARTS: list[dict] = [{"kind": "text", "text": TEXT_PROBE_PROMPT}]
106
+ IMAGE_PROBE_PARTS: list[dict] = [
107
+ {"kind": "text", "text": IMAGE_PROBE_PROMPT},
108
+ {"kind": "file", "file": {
109
+ "bytes": IMAGE_PROBE_PNG_B64,
110
+ "mimeType": "image/png",
111
+ "name": "red.png",
112
+ }},
113
+ ]
114
+ IMAGE_JPEG_PROBE_PARTS: list[dict] = [
115
+ {"kind": "text", "text": IMAGE_PROBE_PROMPT},
116
+ {"kind": "file", "file": {
117
+ "bytes": IMAGE_PROBE_JPEG_B64,
118
+ "mimeType": "image/jpeg",
119
+ "name": "red.jpg",
120
+ }},
121
+ ]
122
+ IMAGE_GIF_PROBE_PARTS: list[dict] = [
123
+ {"kind": "text", "text": IMAGE_PROBE_PROMPT},
124
+ {"kind": "file", "file": {
125
+ "bytes": IMAGE_PROBE_GIF_B64,
126
+ "mimeType": "image/gif",
127
+ "name": "red.gif",
128
+ }},
129
+ ]
130
+ AUDIO_WAV_PROBE_PARTS: list[dict] = [
131
+ {"kind": "text", "text": AUDIO_PROBE_PROMPT},
132
+ {"kind": "file", "file": {
133
+ "bytes": AUDIO_PROBE_WAV_B64,
134
+ "mimeType": "audio/wav",
135
+ "name": "clip.wav",
136
+ }},
137
+ ]
138
+ AUDIO_MP3_PROBE_PARTS: list[dict] = [
139
+ {"kind": "text", "text": AUDIO_PROBE_PROMPT},
140
+ {"kind": "file", "file": {
141
+ "bytes": AUDIO_PROBE_MP3_B64,
142
+ "mimeType": "audio/mpeg",
143
+ "name": "clip.mp3",
144
+ }},
145
+ ]
146
+ AUDIO_M4A_PROBE_PARTS: list[dict] = [
147
+ {"kind": "text", "text": AUDIO_PROBE_PROMPT},
148
+ {"kind": "file", "file": {
149
+ "bytes": AUDIO_PROBE_M4A_B64,
150
+ "mimeType": "audio/mp4",
151
+ "name": "clip.m4a",
152
+ }},
153
+ ]
154
+ AUDIO_OGG_PROBE_PARTS: list[dict] = [
155
+ {"kind": "text", "text": AUDIO_PROBE_PROMPT},
156
+ {"kind": "file", "file": {
157
+ "bytes": AUDIO_PROBE_OGG_B64,
158
+ "mimeType": "audio/ogg",
159
+ "name": "clip.ogg",
160
+ }},
161
+ ]
162
+ PDF_PROBE_PARTS: list[dict] = [
163
+ {"kind": "text", "text": PDF_PROBE_PROMPT},
164
+ {"kind": "file", "file": {
165
+ "bytes": PDF_PROBE_PDF_B64,
166
+ "mimeType": "application/pdf",
167
+ "name": "document.pdf",
168
+ }},
169
+ ]
170
+
171
+
172
+ class VerifyA2AModalitiesStep(TaskStep):
173
+ """Grade modality probes by reading `context.prompt_responses`.
174
+
175
+ `probes` is a list of dicts, each `{"modality": str, "prompt_id": str,
176
+ "expected": str}`. Each `prompt_id` should match an upstream
177
+ `PromptAgentTaskStep`'s `prompt_id`.
178
+ """
179
+
180
+ type: ClassVar[str] = "verify_a2a_modalities"
181
+ entity_refs = (EntityRef.agent("a2a_agent_id"),)
182
+
183
+ def __init__(
184
+ self,
185
+ id: str,
186
+ version: Optional[int],
187
+ a2a_agent_id: str,
188
+ probes: list[dict],
189
+ depends_on: Optional[list[TaskStepDependency]] = None,
190
+ fail_task_on_error: bool = True,
191
+ ):
192
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
193
+ self.a2a_agent_id = a2a_agent_id
194
+ self.probes = probes
195
+
196
+ def to_dict(self) -> dict:
197
+ base = super().to_dict()
198
+ base["a2a_agent_id"] = self.a2a_agent_id
199
+ base["probes"] = self.probes
200
+ return base
201
+
202
+ @classmethod
203
+ def from_dict(cls, data: dict) -> VerifyA2AModalitiesStep:
204
+ return cls(
205
+ **cls._base_from_dict(data),
206
+ a2a_agent_id=data["a2a_agent_id"],
207
+ probes=data["probes"],
208
+ )
209
+
210
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
211
+ from agent_env.a2a_agent import A2AAgent
212
+
213
+ results: dict[str, dict] = {}
214
+ for probe in self.probes:
215
+ modality = probe["modality"]
216
+ prompt_id = probe["prompt_id"]
217
+ expected = probe["expected"]
218
+ response = next(
219
+ (r for r in context.prompt_responses if r.prompt_id == prompt_id),
220
+ None,
221
+ )
222
+ if response is None:
223
+ # Upstream PromptAgentTaskStep crashed before appending its response
224
+ # (e.g., wrapper returned HTTP 4xx on message/send).
225
+ results[modality] = {
226
+ "supported": False,
227
+ "reason": "probe_step_did_not_run",
228
+ }
229
+ elif response.error_type:
230
+ results[modality] = {
231
+ "supported": False,
232
+ "reason": "protocol_reject",
233
+ "error_type": response.error_type,
234
+ "probe_response": (response.response or "")[:200],
235
+ }
236
+ elif expected.lower() in (response.response or "").lower():
237
+ results[modality] = {
238
+ "supported": True,
239
+ "reason": "passed",
240
+ "probe_response": response.response[:200],
241
+ }
242
+ else:
243
+ results[modality] = {
244
+ "supported": False,
245
+ "reason": "no_ingestion_evidence",
246
+ "probe_response": (response.response or "")[:200],
247
+ }
248
+ logger.info(
249
+ f"Modality '{modality}' graded: supported={results[modality]['supported']} "
250
+ f"reason={results[modality]['reason']}"
251
+ )
252
+
253
+ # Echo what the AgentCard declared, for cross-reference with the
254
+ # actual probe results.
255
+ agent = A2AAgent.get(self.a2a_agent_id)
256
+ card = agent.metadata.get("agent_card") or {}
257
+ declared = {
258
+ "default_input_modes": card.get("defaultInputModes"),
259
+ "default_output_modes": card.get("defaultOutputModes"),
260
+ }
261
+
262
+ agent.update_metadata({
263
+ **agent.metadata,
264
+ "validated_modalities": {"input": results, "declared": declared},
265
+ })
266
+ context.metadata.setdefault("verifications", {})["a2a_modalities"] = {
267
+ "input": results,
268
+ "declared": declared,
269
+ }
270
+ return context
@@ -0,0 +1,102 @@
1
+ """Verify the A2A peer-agents extension end-to-end."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import ClassVar, Optional
6
+
7
+ import httpx
8
+
9
+ from agent_env.a2a_agent import A2AAgent
10
+ from agent_env.task_step.context import TaskStepContext
11
+ from agent_env.entity_refs import EntityRef
12
+ from agent_env.task_step.task_step import TaskStep, TaskStepDependency
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class VerifyA2APeerAgentsStep(TaskStep):
18
+ """Consolidate four checks of the peer-agents extension and stamp results on agent.metadata.
19
+
20
+ Assumes upstream steps have deployed the agent under test plus a peer-target
21
+ agent, peered them via PeerAgentsTaskStep, and prompted the agent under test
22
+ to use peer_send_message to relay an `expected_token` from the peer."""
23
+
24
+ type: ClassVar[str] = "verify_a2a_peer_agents"
25
+ entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
26
+
27
+ def __init__(self, id: str, version: Optional[int], a2a_agent_id: str, a2a_agent_version: Optional[int], peer_prompt_id: str, expected_token: str, depends_on: Optional[list[TaskStepDependency]] = None, fail_task_on_error: bool = True):
28
+ super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
29
+ self.a2a_agent_id = a2a_agent_id
30
+ self.a2a_agent_version = a2a_agent_version
31
+ self.peer_prompt_id = peer_prompt_id
32
+ self.expected_token = expected_token
33
+
34
+ def to_dict(self) -> dict:
35
+ return {**super().to_dict(), "a2a_agent_id": self.a2a_agent_id, "a2a_agent_version": self.a2a_agent_version, "peer_prompt_id": self.peer_prompt_id, "expected_token": self.expected_token}
36
+
37
+ @classmethod
38
+ def from_dict(cls, data: dict) -> "VerifyA2APeerAgentsStep":
39
+ return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], a2a_agent_version=data.get("a2a_agent_version"), peer_prompt_id=data["peer_prompt_id"], expected_token=data["expected_token"])
40
+
41
+ async def execute(self, context: TaskStepContext) -> TaskStepContext:
42
+ deployed = next((a for a in context.deployed_agents if a.agent_name == TaskStep.DEFAULT_AGENT_NAME), None)
43
+ if deployed is None:
44
+ raise RuntimeError(f"VerifyA2APeerAgentsStep: no deployed agent named '{TaskStep.DEFAULT_AGENT_NAME}' in context")
45
+ a2a_url = deployed.a2a_url or deployed.api_url
46
+ if not a2a_url:
47
+ raise RuntimeError(f"VerifyA2APeerAgentsStep: deployed agent has no a2a_url")
48
+
49
+ extension = A2AAgent.find_extension(deployed.a2a_card or {}, A2AAgent.EXT_PEER_AGENTS)
50
+ extension_advertised = extension is not None
51
+ endpoint = (extension.get("params") or {}).get("endpoint", "/ext/peer-agents") if extension else "/ext/peer-agents"
52
+ post_ok, list_ok = await self._check_round_trip(a2a_url, endpoint)
53
+ sidecar_registered = await self._check_sidecar_registered(a2a_url)
54
+ peer_call_ok, response_excerpt = self._check_peer_call(context)
55
+
56
+ peer_entry = {
57
+ "supported": extension_advertised and post_ok and list_ok and sidecar_registered and peer_call_ok,
58
+ "extension_advertised": extension_advertised,
59
+ "methods": {"set": {"supported": post_ok}, "list": {"supported": list_ok}},
60
+ "sidecar_self_registered": sidecar_registered,
61
+ "peer_call_succeeded": peer_call_ok,
62
+ "peer_call_response_excerpt": response_excerpt,
63
+ }
64
+
65
+ agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
66
+ validated_ext = agent.metadata.get("validated_a2a_extensions", {})
67
+ validated_ext[A2AAgent.EXT_PEER_AGENTS] = peer_entry
68
+ agent.update_metadata({**agent.metadata, "validated_a2a_extensions": validated_ext})
69
+
70
+ context.metadata.setdefault("verifications", {})["a2a_peer_agents"] = peer_entry
71
+ logger.info(f"peer-agents validation: {peer_entry}")
72
+ return context
73
+
74
+ @staticmethod
75
+ async def _check_round_trip(a2a_url: str, endpoint: str) -> tuple[bool, bool]:
76
+ probe = {"peers": [{"name": "probe", "url": "http://example.invalid"}]}
77
+ post_ok = list_ok = False
78
+ async with httpx.AsyncClient() as client:
79
+ try:
80
+ post_resp = await client.post(f"{a2a_url}{endpoint}", json=probe, timeout=30)
81
+ post_ok = post_resp.status_code < 400
82
+ get_resp = await client.get(f"{a2a_url}{endpoint}", timeout=30)
83
+ list_ok = get_resp.status_code < 400 and any(p.get("name") == "probe" for p in (get_resp.json().get("peers") or []))
84
+ finally:
85
+ await client.post(f"{a2a_url}{endpoint}", json={"peers": []}, timeout=30)
86
+ return post_ok, list_ok
87
+
88
+ @staticmethod
89
+ async def _check_sidecar_registered(a2a_url: str) -> bool:
90
+ async with httpx.AsyncClient() as client:
91
+ resp = await client.get(f"{a2a_url}/ext/mcp-config", timeout=30)
92
+ if resp.status_code >= 400:
93
+ return False
94
+ servers = (resp.json() or {}).get("mcp_servers") or {}
95
+ return any("127.0.0.1" in (info.get("url") or "") and (info.get("url") or "").endswith("/mcp") for info in servers.values())
96
+
97
+ def _check_peer_call(self, context: TaskStepContext) -> tuple[bool, str]:
98
+ prompt_resp = next((p for p in context.prompt_responses if p.prompt_id == self.peer_prompt_id), None)
99
+ if prompt_resp is None:
100
+ return False, ""
101
+ response = prompt_resp.response or ""
102
+ return self.expected_token in response, response[:200]