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
agent_env/env/env.py ADDED
@@ -0,0 +1,433 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import posixpath
5
+ import shlex
6
+ from abc import ABC
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Self
9
+
10
+ from agentenv_protocol import client as protocol_v1
11
+
12
+ from agent_env.entity_refs import EntityRef
13
+ from agent_env.utils.paths import validate_relative_filename
14
+
15
+ from .gateway import GatewayMode
16
+ from .gateway.constants import WELL_KNOWN_PATH
17
+ from .store import get_env_store
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Default timeout for one invoke() call; above the class because a default argument needs it.
22
+ _EXTENSION_CALL_TIMEOUT_S = 30
23
+
24
+ if TYPE_CHECKING:
25
+ from agent_env.bundle.authoring import AuthoringContext
26
+ from .store import EnvQuery
27
+
28
+
29
+ class EnvCapabilityUnsupported(RuntimeError):
30
+ """A deployed env's card does not offer a capability method its caller needs."""
31
+
32
+ def __init__(self, capability: str, method: str, env_id: str, instance_id: Optional[str] = None,
33
+ environment_name: Optional[str] = None) -> None:
34
+ self.capability = capability
35
+ self.method = method
36
+ self.env_id = env_id
37
+ self.instance_id = instance_id
38
+ self.environment_name = environment_name
39
+ child = f" child env '{environment_name}'" if environment_name else ""
40
+ super().__init__(f"env '{env_id}'{child} does not offer '{method}' on {capability}.")
41
+
42
+
43
+ class EnvNeedsGateway(RuntimeError):
44
+ """A caller reaches an env through its gateway, and the env was deployed without one."""
45
+
46
+ def __init__(self, what: str, env_id: Optional[str] = None) -> None:
47
+ self.what = what
48
+ self.env_id = env_id
49
+ env = f"env '{env_id}'" if env_id else "this env"
50
+ super().__init__(f"{what} needs a gateway; {env} was deployed without one")
51
+
52
+
53
+ class EnvNeedsSandbox(RuntimeError):
54
+ """A caller acts on an env's sandbox, and the env runs outside agent-env's sandboxes."""
55
+
56
+ def __init__(self, what: str, env_id: str) -> None:
57
+ self.what = what
58
+ self.env_id = env_id
59
+ super().__init__(f"{what} needs a sandbox; env '{env_id}' runs outside agent-env's sandboxes")
60
+
61
+
62
+ @dataclass(kw_only=True)
63
+ class DeployedEnv:
64
+ """A deployed env: the env, its card, and the registry's bookkeeping. Each topology's subclass adds what it created."""
65
+
66
+ env_id: str
67
+ env_version: int
68
+ # The provider that made the record, so the class it loads as (see from_dict); capabilities still come from the card.
69
+ env_provider_type: Optional[str] = None
70
+ environment_card_url: Optional[str] = None
71
+ # The env card as last read, and when (ISO 8601, UTC); written at deploy from the readiness probe.
72
+ environment_card: Optional[dict] = None
73
+ environment_card_read_at_utc: Optional[str] = None
74
+ # From the card whenever the record carries it (__post_init__); stored because readers of the JSON use them.
75
+ mcp_url: Optional[str] = None
76
+ mcp_server_name: Optional[str] = None
77
+ # Set by the registry and the deploy step, never by a provider.
78
+ metadata: Optional[dict] = None
79
+ instance_id: Optional[str] = None
80
+ created_at_utc: Optional[str] = None
81
+ expires_at_utc: Optional[str] = None
82
+
83
+ def __post_init__(self) -> None:
84
+ # Only records from before #1137 (2026-09-24) lack their card: they keep the stored values.
85
+ if self.environment_card and self.environment_card_url:
86
+ self.mcp_url = _mcp_url(self.environment_url, self.environment_card)
87
+ self.mcp_server_name = self.environment_card.get("name")
88
+
89
+ @classmethod
90
+ def from_dict(cls, data: dict) -> DeployedEnv:
91
+ record_class = _record_class(data)
92
+ return record_class(**record_class._fields_from(data))
93
+
94
+ @property
95
+ def environment_url(self) -> Optional[str]:
96
+ """The env's address: its card URL without the well-known path; card paths join onto it."""
97
+ return _environment_url(self.environment_card_url) if self.environment_card_url else None
98
+
99
+ def supports(self, uri: str, method: str) -> bool:
100
+ """Whether the stored env card offers `method` on the extension at `uri`; no network."""
101
+ return protocol_v1.find_extension_method(self.environment_card or {}, uri, method) is not None
102
+
103
+ def require(self, uri: str, method: str) -> None:
104
+ if not self.supports(uri, method):
105
+ raise EnvCapabilityUnsupported(uri, method, self.env_id, self.instance_id)
106
+
107
+ def get_child_env_card(self, environment_name: str) -> Optional[dict]:
108
+ """The stored card's child env named `environment_name`; a leaf card is its own only child env."""
109
+ card = self.environment_card or {}
110
+ if not isinstance(card.get("children_environments"), list):
111
+ return card if card.get("name") == environment_name else None
112
+ return protocol_v1.find_child(card, environment_name)
113
+
114
+ async def invoke(self, uri: str, method: str, params: Optional[dict] = None, *,
115
+ environment_name: Optional[str] = None, timeout: int = _EXTENSION_CALL_TIMEOUT_S) -> Any:
116
+ """Call `method` on the extension at `uri` as the env card, or a child env's card, advertises it."""
117
+ card = self.environment_card if environment_name is None else self.get_child_env_card(environment_name)
118
+ if card is None or not self.environment_url or protocol_v1.find_extension_method(card, uri, method) is None:
119
+ raise EnvCapabilityUnsupported(uri, method, self.env_id, self.instance_id, environment_name)
120
+ return await protocol_v1.invoke_extension(self.environment_url, card, uri, params, timeout, method=method)
121
+
122
+ @classmethod
123
+ def _fields_from(cls, data: dict) -> dict:
124
+ return dict(
125
+ env_id=data["env_id"],
126
+ env_version=data["env_version"],
127
+ env_provider_type=data.get("env_provider_type"),
128
+ environment_card_url=data.get("environment_card_url"),
129
+ environment_card=data.get("environment_card"),
130
+ environment_card_read_at_utc=data.get("environment_card_read_at_utc"),
131
+ mcp_url=data.get("mcp_url"),
132
+ mcp_server_name=data.get("mcp_server_name"),
133
+ metadata=data.get("metadata"),
134
+ instance_id=data.get("instance_id"),
135
+ created_at_utc=data.get("created_at_utc"),
136
+ expires_at_utc=data.get("expires_at_utc"),
137
+ )
138
+
139
+
140
+ @dataclass(kw_only=True)
141
+ class DeployedSandboxEnv(DeployedEnv):
142
+ """A deployed env running in our sandboxes: what restore, teardown and the reapers act on."""
143
+
144
+ sandbox_id: str
145
+ sandbox_type: Optional[str] = None
146
+ sandbox_ids: dict[str, str | dict[str, str]] = field(default_factory=dict)
147
+
148
+ @classmethod
149
+ def _fields_from(cls, data: dict) -> dict:
150
+ return {
151
+ **super()._fields_from(data),
152
+ "sandbox_id": data.get("sandbox_id"),
153
+ "sandbox_type": data.get("sandbox_type"),
154
+ "sandbox_ids": data.get("sandbox_ids") or {},
155
+ }
156
+
157
+
158
+ @dataclass(kw_only=True)
159
+ class DeployedGatewayEnv(DeployedSandboxEnv):
160
+ """A deployed env fronted by a gateway: the gateway's URLs and mode, and the state store its deploy holds."""
161
+
162
+ env_provider_type: Optional[str] = "gateway"
163
+ gateway_url: str
164
+ gateway_mode: str = GatewayMode.PERFORMANCE.value
165
+ db_web_url: Optional[str] = None
166
+ db_mcp_url: Optional[str] = None
167
+ website_frontend_urls: Optional[dict[str, str]] = None
168
+ vnc_url: Optional[str] = None
169
+ # Ids of the EnvStateInstance record(s) this deploy's state was provisioned into
170
+ # (one today — one backend per deploy). The forward pointer deploy -> state store.
171
+ env_state_instance_ids: list[str] = field(default_factory=list)
172
+
173
+ @classmethod
174
+ def _fields_from(cls, data: dict) -> dict:
175
+ return {
176
+ **super()._fields_from(data),
177
+ "env_provider_type": data.get("env_provider_type") or "gateway",
178
+ "gateway_url": data.get("gateway_url"),
179
+ "gateway_mode": data.get("gateway_mode", GatewayMode.PERFORMANCE.value),
180
+ "db_web_url": data.get("db_web_url"),
181
+ "db_mcp_url": data.get("db_mcp_url"),
182
+ "website_frontend_urls": data.get("website_frontend_urls"),
183
+ "vnc_url": data.get("vnc_url"),
184
+ "env_state_instance_ids": data.get("env_state_instance_ids") or [],
185
+ }
186
+
187
+
188
+ @dataclass
189
+ class LoadEnvironmentUniverseArtifactResult:
190
+ metadata_filepaths: dict[str, str] = field(default_factory=dict)
191
+ # Which path the load actually took. A snapshot restore swaps a pre-ingested servicedb
192
+ # image; a miss re-ingests every service over HTTP under a 600s per-service cap. The
193
+ # two are comparable in wall-clock on a well-sized VM (measured ~4min vs ~3.5min on a
194
+ # 13-service / ~8.7GB universe) -- what differs is the risk: only the re-ingest can
195
+ # blow a timeout, and only it needs the CPU. Reported so callers can tell the two
196
+ # apart, since otherwise an identical step silently costs minutes or fails.
197
+ restored_from_snapshot: bool = False
198
+ snapshot_db_image_artifact_id: str | None = None
199
+ # Set only on the re-ingest path: whether a reusable snapshot was baked afterwards, so
200
+ # the *next* load can restore instead. None means no bake was attempted.
201
+ snapshot_baked: bool | None = None
202
+ snapshot_bake_error: str | None = None
203
+
204
+
205
+ @dataclass
206
+ class LoadFileArtifactUniverseResult:
207
+ """Records what was staged onto the env sandbox by load_file_artifact_universe."""
208
+
209
+ destination_path: str
210
+ files: dict[str, str] = field(default_factory=dict) # filename -> absolute dest path
211
+
212
+
213
+ class Env(ABC):
214
+ """Base class for all envs."""
215
+
216
+ description: ClassVar[str]
217
+ type: ClassVar[str] = "env"
218
+ toml_refs: ClassVar[tuple[EntityRef, ...]] = ()
219
+ env_provider_types: ClassVar[tuple[str, ...]] = ()
220
+
221
+ def __init__(self, id: str, version: Optional[int], metadata: Optional[dict[str, Any]] = None):
222
+ self.id: str = id
223
+ self.version: Optional[int] = version
224
+ self.metadata: dict[str, Any] = metadata or {}
225
+ self._instance_id: Optional[str] = None
226
+
227
+ @classmethod
228
+ def get(cls, id: str, version: Optional[int] = None) -> "Env":
229
+ from .store import get_env_store
230
+ return get_env_store().get(id, version)
231
+
232
+ @classmethod
233
+ async def from_instance_id(cls, instance_id: str) -> "Env":
234
+ from .store import get_env_instance_store
235
+ deployed = get_env_instance_store().get(instance_id)
236
+ env = cls.get(deployed.env_id, deployed.env_version)
237
+ return await type(env).from_deployed_env(deployed)
238
+
239
+ @classmethod
240
+ def put(cls, **kwargs: Any) -> Self:
241
+ from .store import get_env_store
242
+ kwargs.setdefault("version", None)
243
+ instance = cls(**kwargs)
244
+ return get_env_store().put_document(instance)
245
+
246
+ @classmethod
247
+ def query(cls) -> "EnvQuery":
248
+ from .store import EnvQuery, get_env_store
249
+ return EnvQuery(get_env_store())
250
+
251
+ def update_metadata(self, new_metadata: dict[str, Any]) -> None:
252
+ """Replace this env's metadata in the database using compare-and-swap.
253
+
254
+ The current in-memory metadata is used as the expected old value.
255
+ On success, self.metadata is updated to new_metadata.
256
+
257
+ Raises:
258
+ ValueError: If the env has not been saved (version is None).
259
+ ConcurrentModificationError: If metadata was modified since this env was loaded.
260
+ """
261
+ from .store import get_env_store
262
+ if self.version is None:
263
+ raise ValueError("Cannot update metadata on an unsaved env (version is None)")
264
+ self.metadata = get_env_store().update_metadata(self.id, self.version, self.metadata, new_metadata)
265
+
266
+ def merge_metadata(self, updates: dict[str, Any], retries: int = 3) -> None:
267
+ """Merge ``updates`` into this env's metadata, re-reading and retrying on a
268
+ lost CAS. Use instead of ``update_metadata`` when writing one key of a doc
269
+ another writer may be updating concurrently.
270
+ """
271
+ from agent_env.store.base import ConcurrentModificationError
272
+ from .store import get_env_store
273
+ if self.version is None:
274
+ raise ValueError("Cannot update metadata on an unsaved env (version is None)")
275
+ metadata = self.metadata
276
+ for attempt in range(retries + 1):
277
+ try:
278
+ self.metadata = get_env_store().update_metadata(
279
+ self.id, self.version, metadata, {**metadata, **updates}
280
+ )
281
+ return
282
+ except ConcurrentModificationError:
283
+ if attempt == retries:
284
+ raise
285
+ metadata = Env.get(self.id, self.version).metadata
286
+
287
+ def to_dict(self) -> dict[str, Any]:
288
+ return {"id": self.id, "type": self.type, "version": self.version, "metadata": self.metadata}
289
+
290
+ @classmethod
291
+ def from_dict(cls, data: dict[str, Any]) -> "Env":
292
+ raise NotImplementedError(f"{cls.__name__} must implement from_dict")
293
+
294
+ @classmethod
295
+ def from_toml(cls, data: dict[str, Any], ctx: AuthoringContext) -> "Env":
296
+ """Write the env authored as ``data`` (its toml, with the keys ``toml_refs`` declares resolved
297
+ to ids) under ``ctx.id`` and return it. The default reads ``data`` as ``from_dict`` reads a
298
+ stored document; a type whose toml differs overrides this."""
299
+ return get_env_store().put_document(cls.from_dict({**data, "id": ctx.id, "version": None}))
300
+
301
+ async def deploy(self, **kwargs) -> DeployedEnv:
302
+ raise NotImplementedError(f"{type(self).__name__} must implement deploy()")
303
+
304
+ @classmethod
305
+ async def from_deployed_env(cls, deployed: DeployedEnv) -> Self:
306
+ """The env a running deployment serves, for its caller to drive. A type that can be
307
+ deployed on its own overrides this; one deployed only inside another, or never, need not.
308
+ An override takes ``deployed: DeployedEnv`` and narrows it with ``isinstance``.
309
+ """
310
+ raise NotImplementedError(f"{cls.__name__} cannot be rebuilt from a deployment")
311
+
312
+ async def reset(self, deployed: "DeployedEnv") -> None:
313
+ """Return an already-deployed env to a clean between-tasks state.
314
+
315
+ Base raises so the ``reset_env`` step treats it as unsupported; envs that
316
+ can reset in place override this.
317
+ """
318
+ raise NotImplementedError(f"{type(self).__name__} does not support reset()")
319
+
320
+ async def load_file_artifact_universe(
321
+ self,
322
+ file_artifact_universe: "Any",
323
+ destination_path: Optional[str] = None,
324
+ ) -> "LoadFileArtifactUniverseResult":
325
+ """Stage every FileArtifact in `file_artifact_universe` onto this env's sandbox VM.
326
+
327
+ Files land at ``<destination>/<filename>``. Nested relative paths
328
+ (e.g. ``subdir/file.txt``) are supported — parent dirs are created
329
+ as needed. Each file is fetched directly from S3 by the sandbox via
330
+ a presigned URL, so we never round-trip the bytes through this
331
+ process.
332
+
333
+ Default implementation uses ``self._sandbox`` (set by ``deploy()`` /
334
+ ``from_deployed_env()`` on most env subclasses). Subclasses that
335
+ manage several sandboxes, or none, should override.
336
+ """
337
+ sandbox = getattr(self, "_sandbox", None)
338
+ if sandbox is None:
339
+ raise RuntimeError(
340
+ f"{type(self).__name__} has no _sandbox attribute; either call "
341
+ "deploy() / from_deployed_env() first or override "
342
+ "load_file_artifact_universe in this env class"
343
+ )
344
+
345
+ destination = (destination_path or "/tmp/file_artifacts").rstrip("/") or "/"
346
+
347
+ file_artifacts = file_artifact_universe.get_file_artifacts()
348
+ if not file_artifacts:
349
+ logger.warning(
350
+ f"FileArtifactUniverse '{file_artifact_universe.id}' "
351
+ f"v{file_artifact_universe.version} has no files; nothing to load"
352
+ )
353
+ return LoadFileArtifactUniverseResult(destination_path=destination, files={})
354
+
355
+ dirs_to_make = {destination}
356
+ loaded: dict[str, str] = {}
357
+ for filename in file_artifacts:
358
+ validate_relative_filename(filename)
359
+ dest_path = posixpath.join(destination, filename)
360
+ parent = posixpath.dirname(dest_path)
361
+ if parent:
362
+ dirs_to_make.add(parent)
363
+ loaded[filename] = dest_path
364
+
365
+ mkdir_cmd = " && ".join(f"mkdir -p {shlex.quote(d)}" for d in sorted(dirs_to_make))
366
+ await sandbox.exec_script(mkdir_cmd)
367
+
368
+ total = len(file_artifacts)
369
+ logger.info(
370
+ f"Loading FileArtifactUniverse '{file_artifact_universe.id}' "
371
+ f"v{file_artifact_universe.version} ({total} file(s)) at {destination}"
372
+ )
373
+ for idx, (filename, file_artifact) in enumerate(file_artifacts.items(), 1):
374
+ dest_path = loaded[filename]
375
+ logger.info(f" [{idx}/{total}] {file_artifact.object_url} -> {dest_path}")
376
+ await sandbox.load_s3_file(file_artifact.object_url, dest_path)
377
+
378
+ logger.info(
379
+ f"Loaded FileArtifactUniverse '{file_artifact_universe.id}' "
380
+ f"v{file_artifact_universe.version}: {total} file(s) at {destination}"
381
+ )
382
+ return LoadFileArtifactUniverseResult(destination_path=destination, files=loaded)
383
+
384
+
385
+ def gateway_url_of(deployed: Optional[DeployedEnv]) -> Optional[str]:
386
+ """The gateway's URL, or None for a record without a gateway."""
387
+ return deployed.gateway_url if isinstance(deployed, DeployedGatewayEnv) else None
388
+
389
+
390
+ def require_gateway_url(deployed: DeployedEnv, what: str) -> str:
391
+ """The gateway's URL, or EnvNeedsGateway naming `what` for a record without a gateway."""
392
+ if url := gateway_url_of(deployed):
393
+ return url
394
+ raise EnvNeedsGateway(what, deployed.env_id)
395
+
396
+
397
+ def require_sandbox(deployed: DeployedEnv, what: str) -> DeployedSandboxEnv:
398
+ """The record of an env in one of agent-env's sandboxes, or EnvNeedsSandbox naming `what` for one outside them."""
399
+ if isinstance(deployed, DeployedSandboxEnv):
400
+ return deployed
401
+ raise EnvNeedsSandbox(what, deployed.env_id)
402
+
403
+
404
+ def _environment_url(card_url: str) -> str:
405
+ """The env's address: its card URL without the well-known path, where the protocol puts the card."""
406
+ return card_url.removesuffix(WELL_KNOWN_PATH).rstrip("/")
407
+
408
+
409
+ def _mcp_url(url: str, card: dict) -> str:
410
+ """The MCP endpoint the card declares, joined onto the env's URL with exactly one slash.
411
+
412
+ Plain joining, not URL resolution: resolving "/mcp" would drop a path prefix such as a sandbox
413
+ proxy's /sandbox/<id>, and resolving a relative path would replace the URL's last segment.
414
+ """
415
+ return f"{url.rstrip('/')}/{protocol_v1.mcp_path(card).lstrip('/')}"
416
+
417
+
418
+ def _record_class(data: dict) -> type[DeployedEnv]:
419
+ """A stored record's class: its provider type's, else its shape's, so a record from before the type (or of one unknown here) keeps every field."""
420
+ from agent_env.providers.env_providers.env_provider import record_class_for # providers import this module
421
+
422
+ provider_type = data.get("env_provider_type")
423
+ record_class = record_class_for(provider_type)
424
+ if record_class is not None:
425
+ return record_class
426
+ if any(data.get(name) for name in _GATEWAY_ONLY_FIELDS):
427
+ return DeployedGatewayEnv
428
+ # Every record from before the type ran in a sandbox; only a newer kernel's type can mean none.
429
+ return DeployedSandboxEnv if data.get("sandbox_id") or not provider_type else DeployedEnv
430
+
431
+
432
+ # A record carrying any of these is a gateway's. Not gateway_mode: an older kernel writes its default into any record it round-trips.
433
+ _GATEWAY_ONLY_FIELDS = ("gateway_url", "db_web_url", "db_mcp_url", "website_frontend_urls", "vnc_url", "env_state_instance_ids")
@@ -0,0 +1,87 @@
1
+ """Store for env-artifact relationship data (e.g. universe compatibility results)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from enum import Enum
7
+ from typing import Any, Optional
8
+
9
+ from agent_env.config import get_config
10
+ from agent_env.store.document_store import Filter, UpdateSpec
11
+
12
+
13
+ class EnvArtifactType(str, Enum):
14
+ UNIVERSE_COMPATIBILITY = "universe_compatibility"
15
+
16
+ ENV_ARTIFACTS_COLLECTION = "env_artifacts"
17
+
18
+
19
+ class EnvArtifactStore:
20
+ def __init__(self) -> None:
21
+ self._indexed = None
22
+
23
+ @property
24
+ def _doc_store(self):
25
+ # Resolved per call: a cached store outlives reset_config(), so a process that
26
+ # re-pointed would read the new config and write the old backend.
27
+ store = get_config().get_document_store()
28
+ if self._indexed is not store:
29
+ store.ensure_index(
30
+ ENV_ARTIFACTS_COLLECTION,
31
+ ["env_id", "env_version", "artifact_id", "artifact_version", "type"],
32
+ unique=True,
33
+ )
34
+ store.ensure_index(ENV_ARTIFACTS_COLLECTION, ["artifact_id", "type"])
35
+ self._indexed = store
36
+ return store
37
+
38
+ def put(self, env_id: str, env_version: int, artifact_id: str, artifact_version: int, type: str, data: dict[str, Any]) -> None:
39
+ filter_key = {"env_id": env_id, "env_version": env_version, "artifact_id": artifact_id, "artifact_version": artifact_version, "type": type}
40
+ self._doc_store.update(
41
+ ENV_ARTIFACTS_COLLECTION,
42
+ Filter.of(**filter_key),
43
+ UpdateSpec(set={**filter_key, "data": data, "created_at_utc": datetime.now(timezone.utc).isoformat()}),
44
+ upsert=True,
45
+ )
46
+
47
+ def get(self, env_id: str, env_version: int, artifact_id: str, artifact_version: int, type: str) -> Optional[dict[str, Any]]:
48
+ return self._doc_store.find_one(
49
+ ENV_ARTIFACTS_COLLECTION,
50
+ Filter.of(env_id=env_id, env_version=env_version, artifact_id=artifact_id, artifact_version=artifact_version, type=type),
51
+ )
52
+
53
+ def get_by_env(self, env_id: str, env_version: int | None = None, type: str | None = None) -> list[dict[str, Any]]:
54
+ query: dict[str, Any] = {"env_id": env_id}
55
+ if env_version is not None:
56
+ query["env_version"] = env_version
57
+ if type is not None:
58
+ query["type"] = type
59
+ return self._doc_store.query(ENV_ARTIFACTS_COLLECTION, Filter.of(**query))
60
+
61
+ def get_by_artifact(self, artifact_id: str, artifact_version: int | None = None, type: str | None = None) -> list[dict[str, Any]]:
62
+ query: dict[str, Any] = {"artifact_id": artifact_id}
63
+ if artifact_version is not None:
64
+ query["artifact_version"] = artifact_version
65
+ if type is not None:
66
+ query["type"] = type
67
+ return self._doc_store.query(ENV_ARTIFACTS_COLLECTION, Filter.of(**query))
68
+
69
+
70
+ _env_artifact_store: Optional[EnvArtifactStore] = None
71
+
72
+
73
+ def get_env_artifact_store() -> EnvArtifactStore:
74
+ global _env_artifact_store
75
+ if _env_artifact_store is None:
76
+ _env_artifact_store = EnvArtifactStore()
77
+ return _env_artifact_store
78
+
79
+
80
+ def set_env_artifact_store(store: EnvArtifactStore) -> None:
81
+ global _env_artifact_store
82
+ _env_artifact_store = store
83
+
84
+
85
+ def reset_env_artifact_store() -> None:
86
+ global _env_artifact_store
87
+ _env_artifact_store = None
@@ -0,0 +1,7 @@
1
+ from .gateway_server import GatewayEnv
2
+ from .mcp_server import MCPServerEnv
3
+ from .multi_env import MultiEnv
4
+ from .service_db import ServiceDBEnv
5
+ from .website import WebsiteEnv
6
+
7
+ __all__ = ["GatewayEnv", "MCPServerEnv", "MultiEnv", "ServiceDBEnv", "WebsiteEnv"]