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,174 @@
1
+ """Shared pieces for the explorer's read routers: every list endpoint is
2
+ ``DocumentStore.latest_per_id`` + ``count_distinct`` behind one factory, so no route
3
+ depends on a backend query language.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Optional
9
+
10
+ from fastapi import APIRouter, HTTPException, Query
11
+ from pydantic import BaseModel
12
+
13
+ from agent_env.artifact.registry import canonical_type, equivalent_types
14
+ from agent_env.config import get_config
15
+ from agent_env.store import Filter, Sort
16
+
17
+
18
+ class PaginatedResponse(BaseModel):
19
+ """Pagination envelope shared by every list endpoint."""
20
+
21
+ items: list[Any]
22
+ total: int
23
+ limit: int
24
+ offset: int
25
+ has_more: bool = False
26
+
27
+
28
+ def docs():
29
+ return get_config().get_document_store()
30
+
31
+
32
+ def _id_matches(doc: dict, id_field: str, needle: str) -> bool:
33
+ """Case-insensitive substring match on the entity id."""
34
+ value = doc.get(id_field)
35
+ return isinstance(value, str) and needle.lower() in value.lower()
36
+
37
+
38
+ _UNIVERSE_FILE_TYPES = {"file_artifact_universe"}
39
+ _UNIVERSE_ENVIRONMENT_TYPES = {"environment_universe"}
40
+
41
+
42
+ def _ref_id_version(ref: Any) -> tuple[Optional[str], Optional[int]]:
43
+ if isinstance(ref, dict):
44
+ return ref.get("id"), ref.get("version")
45
+ return (ref if isinstance(ref, str) else None), None
46
+
47
+
48
+ def _resolve_doc(store, collection: str, ref: Any) -> Optional[dict]:
49
+ """Fetch a referenced artifact doc by (id, version); latest version if unpinned."""
50
+ rid, ver = _ref_id_version(ref)
51
+ if rid is None:
52
+ return None
53
+ if ver is not None:
54
+ return store.find_one(collection, Filter.of(id=rid, version=ver))
55
+ found = store.latest_per_id(collection, Filter.of(id=rid))
56
+ return found[0] if found else None
57
+
58
+
59
+ def _enrich_universe(doc: dict, store, collection: str) -> dict:
60
+ """Resolve a universe's artifact refs into the ``files``/``services`` arrays the UI renders.
61
+
62
+ Each file's ``object_url`` is the FileArtifact's seam location, served via /objects/content.
63
+ """
64
+ t = doc.get("type")
65
+ if collection == "artifacts" and isinstance(t, str):
66
+ t = canonical_type(t)
67
+ if t in _UNIVERSE_FILE_TYPES:
68
+ refs = doc.get("file_artifact_refs") or doc.get("file_artifact_ids") or {}
69
+ files = []
70
+ for filename, ref in refs.items():
71
+ fa = _resolve_doc(store, collection, ref)
72
+ rid, ver = _ref_id_version(ref)
73
+ files.append({
74
+ "filename": filename,
75
+ "artifact_id": rid,
76
+ "version": ver,
77
+ "content_type": fa.get("content_type") if fa else None,
78
+ "object_url": fa.get("s3_url") if fa else None,
79
+ })
80
+ doc["files"] = files
81
+ elif t in _UNIVERSE_ENVIRONMENT_TYPES:
82
+ refs = doc.get("service_artifact_refs")
83
+ if not refs:
84
+ refs = [{"id": sid} for sid in (doc.get("service_artifact_ids") or [])]
85
+ services = []
86
+ for ref in refs:
87
+ sa = _resolve_doc(store, collection, ref)
88
+ rid, ver = _ref_id_version(ref)
89
+ services.append({
90
+ "service_name": (sa.get("service_name") if sa else None) or rid,
91
+ "artifact_id": rid,
92
+ "version": ver,
93
+ })
94
+ doc["services"] = services
95
+ return doc
96
+
97
+
98
+ def versioned_router(
99
+ *,
100
+ prefix: str,
101
+ tag: str,
102
+ collection: str,
103
+ id_field: str = "id",
104
+ ) -> APIRouter:
105
+ """List / get / versions over one ``(id, version)`` collection (artifacts, envs, tasks,
106
+ agents, evals)."""
107
+ router = APIRouter(prefix=prefix, tags=[tag])
108
+
109
+ @router.get("", response_model=PaginatedResponse)
110
+ def list_items(
111
+ limit: int = Query(50, ge=1, le=500),
112
+ offset: int = Query(0, ge=0),
113
+ type: Optional[list[str]] = Query(None, description="Filter by type; repeat to union."),
114
+ id: Optional[str] = Query(None, description="Substring match on the entity id."),
115
+ sort_by: str = Query("created_at_utc"),
116
+ descending: bool = Query(True),
117
+ ) -> PaginatedResponse:
118
+ store = docs()
119
+ want_types = set(type) if type else None
120
+ if want_types is not None and collection == "artifacts":
121
+ want_types = {s for t in want_types for s in equivalent_types(t)}
122
+ if id or want_types is not None:
123
+ # Reduce to the latest version per id FIRST, then filter (by id substring
124
+ # and/or type) in Python. Filtering before the reduction could select a
125
+ # superseded version whose type still matches and report it as the live
126
+ # entity; reducing first means the type filter only ever sees live rows.
127
+ reduced = store.latest_per_id(
128
+ collection, Filter({}), id_field=id_field,
129
+ sort=Sort.by(sort_by, descending=descending),
130
+ )
131
+ matched = [
132
+ d for d in reduced
133
+ if (not id or _id_matches(d, id_field, id))
134
+ and (want_types is None or str(d.get("type")) in want_types)
135
+ ]
136
+ total = len(matched)
137
+ items = matched[offset: offset + limit] if limit else matched[offset:]
138
+ else:
139
+ items = store.latest_per_id(
140
+ collection, Filter({}), id_field=id_field,
141
+ sort=Sort.by(sort_by, descending=descending), limit=limit, offset=offset,
142
+ )
143
+ # Count distinct ids, not rows — one entity (N versions) is one result.
144
+ total = store.count_distinct(collection, Filter({}), id_field=id_field)
145
+ return PaginatedResponse(
146
+ items=items, total=total, limit=limit, offset=offset,
147
+ has_more=offset + len(items) < total,
148
+ )
149
+
150
+ @router.get("/{entity_id}")
151
+ def get_item(entity_id: str, version: Optional[int] = None) -> dict:
152
+ store = docs()
153
+ if version is not None:
154
+ doc = store.find_one(collection, Filter.of(**{id_field: entity_id, "version": version}))
155
+ else:
156
+ found = store.latest_per_id(collection, Filter.of(**{id_field: entity_id}), id_field=id_field)
157
+ doc = found[0] if found else None
158
+ if doc is None:
159
+ raise HTTPException(status_code=404, detail=f"{tag} {entity_id} not found")
160
+ return _enrich_universe(doc, store, collection)
161
+
162
+ @router.get("/{entity_id}/versions")
163
+ def list_versions(
164
+ entity_id: str,
165
+ limit: int = Query(100, ge=1, le=500),
166
+ offset: int = Query(0, ge=0),
167
+ ) -> list[dict]:
168
+ """Every stored version of one entity, newest first. Returns a bare array (not a
169
+ PaginatedResponse) — clients expect a list here."""
170
+ filt = Filter.of(**{id_field: entity_id})
171
+ return docs().query(collection, filt, sort=Sort.by("version", descending=True),
172
+ limit=limit, offset=offset)
173
+
174
+ return router
@@ -0,0 +1,25 @@
1
+ """Read-only A2A conversation transcripts, per task instance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+
7
+ from agent_env.explorer.routers.common import docs
8
+ from agent_env.store import Filter, Sort
9
+
10
+ # Prefix hardcoded (not imported from app) to avoid a router->app import cycle.
11
+ router = APIRouter(prefix="/api/v1", tags=["conversations"])
12
+
13
+ # Written by agent_env.a2a_agent.conversation_store.
14
+ _CONVERSATIONS_COLLECTION = "agent_env_a2a_conversations"
15
+
16
+
17
+ @router.get("/task-instances/{task_instance_id}/conversations")
18
+ def list_conversations(task_instance_id: str) -> dict:
19
+ """A2A conversations recorded for a task instance, oldest first."""
20
+ conversations = docs().query(
21
+ _CONVERSATIONS_COLLECTION,
22
+ Filter.of(task_instance_id=task_instance_id),
23
+ sort=Sort.by("created_at_utc", descending=False), # oldest first
24
+ )
25
+ return {"conversations": conversations}
@@ -0,0 +1,112 @@
1
+ """Object-store byte seam: stream any object-store blob to the browser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import mimetypes
6
+ import os
7
+ import tempfile
8
+ from urllib.parse import quote
9
+
10
+ from fastapi import APIRouter, HTTPException, Query
11
+ from fastapi.responses import StreamingResponse
12
+
13
+ from agent_env.config import get_config
14
+
15
+ # Prefix hardcoded (not imported from app) to avoid a router->app import cycle.
16
+ router = APIRouter(prefix="/api/v1/objects", tags=["objects"])
17
+
18
+ _CONTENT_STREAM_CHUNK = 64 * 1024
19
+
20
+ # Types a browser runs as a document — served under CSP sandbox so a stored
21
+ # HTML/SVG artifact can't execute script on the explorer origin.
22
+ _SANDBOX_TYPES = frozenset({
23
+ "text/html", "application/xhtml+xml", "image/svg+xml",
24
+ "application/xml", "text/xml", "application/xslt+xml",
25
+ })
26
+
27
+
28
+ def _owned_store(object_url: str):
29
+ """The store serving ``object_url``, which must be one of its objects: the explorer reads
30
+ nothing outside the configured store."""
31
+ store = get_config().get_object_store_at(object_url)
32
+ if not store.owns(object_url):
33
+ raise HTTPException(status_code=400, detail="object_url is not in the configured object store")
34
+ if not store.get_object_key(object_url):
35
+ raise HTTPException(status_code=400, detail="object_url names a bucket, not an object")
36
+ return store
37
+
38
+
39
+ def _content_disposition(filename: str) -> str:
40
+ """``inline`` with the name for the browser to save under. Header values are Latin-1, so a
41
+ name that is not plain ASCII goes in RFC 6266's ``filename*`` beside an ASCII fallback."""
42
+ fallback = "".join(c if " " <= c <= "~" and c not in '"\\' else "_" for c in filename)
43
+ if fallback == filename:
44
+ return f'inline; filename="{filename}"'
45
+ return f"inline; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename, safe='')}"
46
+
47
+
48
+ @router.get("/content")
49
+ def object_content(object_url: str = Query(...)):
50
+ """Proxy an object-store blob to the browser, resolved through the configured store."""
51
+ store = _owned_store(object_url)
52
+ meta = store.get_object_metadata_at(object_url)
53
+ if meta is None:
54
+ raise HTTPException(status_code=404, detail="no object at the given object_url")
55
+
56
+ # Stream from a temp file to bound memory on large blobs.
57
+ fd, tmp = tempfile.mkstemp(prefix="agentenv-content-")
58
+ os.close(fd)
59
+ try:
60
+ try:
61
+ store.download_to_file(object_url, tmp)
62
+ except Exception:
63
+ # Object existed at the metadata check above, so a read failure here is an
64
+ # upstream store error (permission/timeout/outage), not a missing object.
65
+ raise HTTPException(status_code=502, detail="object store failed to read the object")
66
+ response = _streamed(tmp, object_url, meta)
67
+ except BaseException:
68
+ # The response owns the file once it exists; until then nothing else will remove it.
69
+ os.unlink(tmp)
70
+ raise
71
+ return response
72
+
73
+
74
+ def _streamed(tmp: str, object_url: str, meta) -> StreamingResponse:
75
+ def _stream_and_cleanup():
76
+ try:
77
+ with open(tmp, "rb") as fh:
78
+ while chunk := fh.read(_CONTENT_STREAM_CHUNK):
79
+ yield chunk
80
+ finally:
81
+ os.unlink(tmp)
82
+
83
+ filename = object_url.rstrip("/").rsplit("/", 1)[-1] or "object"
84
+ # Local stores don't persist a content type; fall back to the filename so both the
85
+ # media type and the sandbox decision below are correct on every backend.
86
+ content_type = meta.content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream"
87
+ headers = {
88
+ "X-Content-Type-Options": "nosniff", # untrusted artifact bytes must not sniff-execute
89
+ "Content-Disposition": _content_disposition(filename),
90
+ }
91
+ if content_type.split(";")[0].strip().lower() in _SANDBOX_TYPES:
92
+ headers["Content-Security-Policy"] = "sandbox"
93
+ # Reads return the bytes as stored, so the browser needs the encoding to decode them.
94
+ if meta.content_encoding:
95
+ headers["Content-Encoding"] = meta.content_encoding
96
+ if meta.size is not None:
97
+ headers["Content-Length"] = str(meta.size)
98
+ return StreamingResponse(
99
+ _stream_and_cleanup(),
100
+ media_type=content_type,
101
+ headers=headers,
102
+ )
103
+
104
+
105
+ @router.get("/metadata")
106
+ def object_metadata(object_url: str = Query(...)):
107
+ """Size + content-type for an object, without transferring the bytes."""
108
+ store = _owned_store(object_url)
109
+ meta = store.get_object_metadata_at(object_url)
110
+ if meta is None:
111
+ raise HTTPException(status_code=404, detail="no object at the given object_url")
112
+ return {"size_bytes": meta.size, "content_type": meta.content_type}