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,282 @@
1
+ import asyncio
2
+
3
+ import click
4
+
5
+ from agent_env.artifact import EnvironmentArtifact, EnvironmentUniverseArtifact
6
+ from agent_env.cli.utils import detect_base_metadata, env_provider_type_option, skips_local_validation
7
+ from agent_env.env import Env, MultiEnv
8
+
9
+
10
+ def parse_env_ref(ref: str) -> tuple[str, int | None]:
11
+ """Parse 'id' or 'id:version' format."""
12
+ if ":" in ref:
13
+ id_part, version_part = ref.split(":", 1)
14
+ return id_part, int(version_part)
15
+ return ref, None
16
+
17
+
18
+ @click.group(name="multi")
19
+ def multi():
20
+ """MultiEnv commands."""
21
+ pass
22
+
23
+
24
+ @multi.command()
25
+ @click.option("--id", "env_id", required=True, help="Env id")
26
+ @click.option("--mcp-server", "mcp_servers", multiple=True, help="MCPServerEnv id[:version]")
27
+ @click.option("--website", "websites", multiple=True, help="WebsiteEnv id[:version]")
28
+ @click.option("--metadata", "metadata_pairs", multiple=True, help="Metadata key=value pair (repeatable)")
29
+ @click.option("--name", default=None, help="Name agents see this env's MCP server under, e.g. crm -> mcp__crm__<tool> (default: env + 4 random digits per deploy)")
30
+ @env_provider_type_option("What deploys the env: 'gateway' (a gateway in front of its servers and websites, whatever their own "
31
+ "env_provider_type), or the type of an installed agent_env.env_providers plugin, which deploys them all", "multi")
32
+ @click.option("--validate", "run_validation", is_flag=True, default=False, help="Validate the environment after registering it")
33
+ def put(env_id: str, mcp_servers: tuple[str, ...], websites: tuple[str, ...], metadata_pairs: tuple[str, ...], name: str | None, env_provider_type: str,
34
+ run_validation: bool):
35
+ """Create a MultiEnv from MCPServerEnv and/or WebsiteEnv ids."""
36
+
37
+ if not mcp_servers and not websites:
38
+ click.echo("Error: At least one of --mcp-server or --website must be provided.", err=True)
39
+ raise SystemExit(1)
40
+
41
+ mcp_server_envs = []
42
+ for ref in mcp_servers:
43
+ server_id, server_version = parse_env_ref(ref)
44
+ click.echo(f"Fetching MCPServerEnv: id={server_id} version={server_version or 'latest'}...")
45
+ env = Env.get(server_id, version=server_version)
46
+ if env.type != "mcp_server":
47
+ click.echo(f"Error: {server_id} is type '{env.type}', expected 'mcp_server'", err=True)
48
+ raise SystemExit(1)
49
+ mcp_server_envs.append(env)
50
+ click.echo(f" Found: id={env.id} version={env.version}")
51
+
52
+ website_envs = []
53
+ for ref in websites:
54
+ website_id, website_version = parse_env_ref(ref)
55
+ click.echo(f"Fetching WebsiteEnv: id={website_id} version={website_version or 'latest'}...")
56
+ env = Env.get(website_id, version=website_version)
57
+ if env.type != "website":
58
+ click.echo(f"Error: {website_id} is type '{env.type}', expected 'website'", err=True)
59
+ raise SystemExit(1)
60
+ website_envs.append(env)
61
+ click.echo(f" Found: id={env.id} version={env.version}")
62
+
63
+ user_metadata = {}
64
+ for pair in metadata_pairs:
65
+ if "=" not in pair:
66
+ raise SystemExit(f"Invalid metadata format '{pair}', expected key=value")
67
+ key, value = pair.split("=", 1)
68
+ user_metadata[key] = value
69
+ metadata = detect_base_metadata()
70
+ metadata.update(user_metadata)
71
+
72
+ refusal = MultiEnv(id=env_id, version=None, mcp_server_envs=mcp_server_envs, website_envs=website_envs, name=name,
73
+ env_provider_type=env_provider_type).deploy_refusal()
74
+ if refusal:
75
+ click.echo(f"Error: {refusal}", err=True)
76
+ raise SystemExit(1)
77
+ click.echo(f"Creating MultiEnv...")
78
+ multi_env = MultiEnv.put(id=env_id, mcp_server_envs=mcp_server_envs, website_envs=website_envs, metadata=metadata if metadata else None, name=name,
79
+ env_provider_type=env_provider_type)
80
+ click.echo(f"Created MultiEnv: id={multi_env.id} version={multi_env.version} env_provider_type={multi_env.env_provider_type}")
81
+ if run_validation and not skips_local_validation(multi_env.id, "env"):
82
+ click.echo("\nValidating environment...")
83
+ instance_id = asyncio.run(multi_env.validate(on_progress=click.echo))
84
+ click.echo(f"Validation task: {instance_id}")
85
+
86
+
87
+ @multi.command(name="load-environment-universe-artifact")
88
+ @click.option("--env-instance-id", "env_instance_id", required=True, help="Deployed env instance ID")
89
+ @click.option("--environment-universe-artifact-id", "environment_universe_artifact_id",
90
+ required=True, help="EnvironmentUniverseArtifact id")
91
+ @click.option("--snapshot-after-load/--no-snapshot-after-load", "snapshot_after_load", default=None,
92
+ help="After a full re-ingest, bake a clean snapshot so later loads of this "
93
+ "universe restore from an image instead of re-ingesting every service. "
94
+ "Defaults to AGENT_ENV_SNAPSHOT_AFTER_LOAD (off).")
95
+ def load_environment_universe_artifact(env_instance_id: str, environment_universe_artifact_id: str,
96
+ snapshot_after_load: bool | None):
97
+ """Load an environment universe artifact into a deployed env."""
98
+ from agent_env.store.base import NotFoundError
99
+
100
+ from agent_env.env.store import get_env_instance_store
101
+ try:
102
+ deployed_env = get_env_instance_store().get(env_instance_id)
103
+ except NotFoundError:
104
+ click.echo(f"Error: Instance '{env_instance_id}' not found", err=True)
105
+ raise SystemExit(1)
106
+
107
+ click.echo(f"Instance: {env_instance_id} (env={deployed_env.env_id} v{deployed_env.env_version})")
108
+ if getattr(deployed_env, "sandbox_type", None):
109
+ click.echo(f"Sandbox backend: {deployed_env.sandbox_type}")
110
+ env = Env.get(deployed_env.env_id, deployed_env.env_version)
111
+ env = asyncio.run(type(env).from_deployed_env(deployed_env))
112
+
113
+ if not hasattr(env, "load_environment_universe_artifact"):
114
+ click.echo(f"Error: Env type '{env.type}' does not support load_environment_universe_artifact", err=True)
115
+ raise SystemExit(1)
116
+
117
+ click.echo(f"Fetching environment universe artifact: id={environment_universe_artifact_id}...")
118
+ universe_artifact = EnvironmentUniverseArtifact.get(environment_universe_artifact_id)
119
+ click.echo(f"Found environment universe artifact: id={universe_artifact.id} version={universe_artifact.version}")
120
+
121
+ click.echo("Loading environment universe artifact...")
122
+ from agent_env.env.envs.multi_env import MultiEnv
123
+
124
+ if isinstance(env, MultiEnv):
125
+ result = asyncio.run(env.load_environment_universe_artifact(
126
+ universe_artifact, snapshot_after_load=snapshot_after_load,
127
+ ))
128
+ else:
129
+ if snapshot_after_load:
130
+ click.echo(f"Note: env type '{env.type}' has no servicedb to snapshot; "
131
+ "--snapshot-after-load ignored")
132
+ result = asyncio.run(env.load_environment_universe_artifact(universe_artifact))
133
+ click.echo("Successfully loaded all environment artifacts into env")
134
+
135
+ # Which path ran decides whether this load could time out at all, so say it out
136
+ # loud. Measured on a 13-service / ~8.7GB universe: restore ~4min, re-ingest
137
+ # ~3.5min on 8 vCPU but a FAILURE on the 1-vCPU default. Restore is not
138
+ # reliably faster -- it is bounded, which is the part that matters.
139
+ if getattr(result, "restored_from_snapshot", False):
140
+ click.echo(f"Restored from snapshot image: {result.snapshot_db_image_artifact_id}")
141
+ else:
142
+ click.echo("Re-ingested every service over HTTP (no clean snapshot matched); each "
143
+ "service was racing its own 600s timeout")
144
+ if result.snapshot_baked:
145
+ click.echo("Baked a clean snapshot; the next load of this universe can restore "
146
+ "from it instead of re-ingesting")
147
+ elif result.snapshot_baked is False:
148
+ click.echo(f"Snapshot bake did not produce a reusable snapshot: {result.snapshot_bake_error}")
149
+
150
+ if result.metadata_filepaths:
151
+ click.echo("Metadata files downloaded:")
152
+ for key, path in result.metadata_filepaths.items():
153
+ click.echo(f" {key} -> {path}")
154
+
155
+
156
+ @multi.command(name="load-environment-artifact")
157
+ @click.option("--env-instance-id", "env_instance_id", required=True, help="Deployed env instance ID")
158
+ @click.option("--environment-artifact-id", "environment_artifact_id",
159
+ required=True, help="EnvironmentArtifact id")
160
+ def load_environment_artifact(env_instance_id: str, environment_artifact_id: str):
161
+ """Load an environment artifact into a deployed env."""
162
+ from agent_env.store.base import NotFoundError
163
+
164
+ from agent_env.env.store import get_env_instance_store
165
+ try:
166
+ deployed_env = get_env_instance_store().get(env_instance_id)
167
+ except NotFoundError:
168
+ click.echo(f"Error: Instance '{env_instance_id}' not found", err=True)
169
+ raise SystemExit(1)
170
+
171
+ click.echo(f"Instance: {env_instance_id} (env={deployed_env.env_id} v{deployed_env.env_version})")
172
+ env = Env.get(deployed_env.env_id, deployed_env.env_version)
173
+ env = asyncio.run(type(env).from_deployed_env(deployed_env))
174
+
175
+ if not hasattr(env, "load_environment_artifact"):
176
+ click.echo(f"Error: Env type '{env.type}' does not support load_environment_artifact", err=True)
177
+ raise SystemExit(1)
178
+
179
+ click.echo(f"Fetching environment artifact: id={environment_artifact_id}...")
180
+ environment_artifact = EnvironmentArtifact.get(environment_artifact_id)
181
+ click.echo(f"Found environment artifact: id={environment_artifact.id} version={environment_artifact.version}")
182
+
183
+ click.echo("Loading environment artifact...")
184
+ asyncio.run(env.load_environment_artifact(environment_artifact))
185
+ click.echo("Loaded environment artifact into env")
186
+
187
+
188
+ @multi.command()
189
+ @click.option("--id", "env_id", required=True, help="MultiEnv id")
190
+ @click.option("--version", "env_version", type=int, default=None, help="Env version (default: latest)")
191
+ def validate(env_id: str, env_version: int | None):
192
+ """Validate a MultiEnv's environment card and persist it to env metadata."""
193
+ from agent_env.store.base import NotFoundError
194
+ from agent_env.task_step.task_steps.env_card_validator import VALIDATED_ENVIRONMENT_CARD_KEY
195
+
196
+ try:
197
+ env = Env.get(env_id, env_version)
198
+ except NotFoundError:
199
+ click.echo(f"Error: Env '{env_id}' not found", err=True)
200
+ raise SystemExit(1)
201
+
202
+ if not isinstance(env, MultiEnv):
203
+ click.echo(f"Error: Env '{env_id}' is type '{env.type}', expected 'multi'", err=True)
204
+ raise SystemExit(1)
205
+
206
+ click.echo(f"Validating env: id={env.id} version={env.version}")
207
+ instance_id = asyncio.run(env.validate(on_progress=click.echo))
208
+ click.echo(f"Task instance: {instance_id}")
209
+
210
+ env = Env.get(env_id, env.version)
211
+ card_val = env.metadata.get(VALIDATED_ENVIRONMENT_CARD_KEY)
212
+ click.secho("\nEnvironment card:", bold=True)
213
+ if not card_val:
214
+ click.echo(" (not validated)")
215
+ elif card_val.get("accessible"):
216
+ missing = [f for f, v in card_val.get("required_fields", {}).items() if not v.get("present")]
217
+ click.secho(f" ACCESSIBLE: {card_val.get('children_count', 0)} child env(s); extensions={card_val.get('extensions', [])}; tools={card_val.get('tools', [])}", fg="green")
218
+ if missing:
219
+ click.secho(f" missing required fields: {missing}", fg="yellow")
220
+ else:
221
+ click.secho(f" NOT ACCESSIBLE: {card_val.get('error', 'no card')}", fg="magenta")
222
+
223
+
224
+ @multi.command(name="validate-universe-compatibility")
225
+ @click.option("--env-id", required=True, help="MultiEnv id")
226
+ @click.option("--env-version", type=int, default=None, help="Env version (default: latest)")
227
+ @click.option("--universe-artifact-id", required=True, help="EnvironmentUniverseArtifact id")
228
+ def validate_universe_compatibility(env_id: str, env_version: int | None, universe_artifact_id: str):
229
+ """Validate universe artifact compatibility with a MultiEnv via load/export roundtrip."""
230
+ from agent_env.store.base import NotFoundError
231
+
232
+ try:
233
+ env = Env.get(env_id, env_version)
234
+ except NotFoundError:
235
+ click.echo(f"Error: Env '{env_id}' not found", err=True)
236
+ raise SystemExit(1)
237
+
238
+ if not isinstance(env, MultiEnv):
239
+ click.echo(f"Error: Env '{env_id}' is type '{env.type}', expected 'multi'", err=True)
240
+ raise SystemExit(1)
241
+
242
+ click.echo(f"Validating env={env.id} v{env.version} universe={universe_artifact_id}")
243
+ instance_id = asyncio.run(env.validate_universe_compatibility(universe_artifact_id=universe_artifact_id, on_progress=click.echo))
244
+ click.echo(f"Task instance: {instance_id}")
245
+
246
+ from agent_env.env.env_artifact_store import get_env_artifact_store
247
+ from agent_env.env.env_artifact_store import EnvArtifactType, get_env_artifact_store
248
+ results = get_env_artifact_store().get_by_env(env_id, env_version=env.version, type=EnvArtifactType.UNIVERSE_COMPATIBILITY)
249
+ doc = next((r for r in results if r["artifact_id"] == universe_artifact_id), None)
250
+ compat = doc["data"] if doc else {}
251
+
252
+ overall = compat.get("compatible")
253
+ click.echo(click.style(f"\nUniverse compatibility: {'COMPATIBLE' if overall else 'INCOMPATIBLE'}", fg="green" if overall else "magenta"))
254
+ for svc_name, svc_result in sorted(compat.get("services", {}).items()):
255
+ svc_compat = svc_result.get("compatible")
256
+ click.echo(click.style(f"\n {svc_name}: {'COMPATIBLE' if svc_compat else 'INCOMPATIBLE'}", fg="green" if svc_compat else "magenta"))
257
+ for issue in svc_result.get("issues", []):
258
+ critical = issue.get("critical")
259
+ color = "red" if critical else "yellow"
260
+ marker = "CRITICAL" if critical else "warning"
261
+ click.echo(click.style(f" [{issue['phase']}] [{marker}] {issue['entity']}.{issue['field']}: {issue['type']} - {issue['detail']}", fg=color))
262
+
263
+ if compat.get("exported_universe_artifact_id"):
264
+ click.echo(f"\nExported universe artifact: {compat['exported_universe_artifact_id']}")
265
+
266
+
267
+ @multi.command(name="compatible-universes")
268
+ @click.option("--env-id", required=True, help="MultiEnv id")
269
+ @click.option("--env-version", type=int, default=None, help="Env version (default: all versions)")
270
+ def compatible_universes(env_id: str, env_version: int | None):
271
+ """List universe compatibility results for an env."""
272
+ from agent_env.env.env_artifact_store import get_env_artifact_store
273
+
274
+ from agent_env.env.env_artifact_store import EnvArtifactType
275
+ results = get_env_artifact_store().get_by_env(env_id, env_version=env_version, type=EnvArtifactType.UNIVERSE_COMPATIBILITY)
276
+ if not results:
277
+ click.echo(f"No compatibility results found for env {env_id}")
278
+ return
279
+ for r in results:
280
+ compat = r["data"].get("compatible", False)
281
+ status = click.style("COMPATIBLE", fg="green") if compat else click.style("INCOMPATIBLE", fg="magenta")
282
+ click.echo(f" universe={r['artifact_id']} v{r['artifact_version']}: {status}")
@@ -0,0 +1,99 @@
1
+ """CLI commands for ServiceDBEnv."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from agent_env.artifact import DockerImageArtifact
8
+ from agent_env.cli.utils import build_platform_option, detect_env_metadata
9
+ from agent_env.env.envs.service_db import ServiceDBEnv
10
+ from agent_env.config import get_config
11
+ from agent_env.utils.docker_build import build_image
12
+
13
+ # Path to ServiceDB Dockerfile
14
+ SERVICE_DB_DOCKERFILE = Path(__file__).parent.parent.parent / "env" / "envs" / "service_db" / "Dockerfile"
15
+ SERVICE_DB_IMAGE_NAME = "agent-env-service-db"
16
+ DB_WEB_DOCKERFILE = Path(__file__).parent.parent.parent / "env" / "envs" / "service_db" / "Dockerfile.db-web"
17
+ DB_WEB_IMAGE_NAME = "agent-env-db-web"
18
+ DB_MCP_DOCKERFILE = Path(__file__).parent.parent.parent / "env" / "envs" / "service_db" / "Dockerfile.db-mcp"
19
+ DB_MCP_IMAGE_NAME = "agent-env-db-mcp"
20
+
21
+
22
+ @click.group(name="service-db")
23
+ def service_db():
24
+ """ServiceDB environment commands."""
25
+ pass
26
+
27
+
28
+ @service_db.command()
29
+ @click.option("--id", "env_id", default=None, help="Env id (default: config default_service_db_env_id)")
30
+ @click.option("--metadata", "metadata_pairs", multiple=True, help="Metadata key=value pair (repeatable)")
31
+ @build_platform_option
32
+ def put(env_id: str, metadata_pairs: tuple[str, ...], build_platform: str):
33
+ """Build and upload a ServiceDB environment."""
34
+ if not env_id:
35
+ env_id = get_config().default_service_db_env_id
36
+
37
+ click.echo(f"Building ServiceDB image from {SERVICE_DB_DOCKERFILE}...")
38
+
39
+ # Build Docker image
40
+ build_image(SERVICE_DB_DOCKERFILE, SERVICE_DB_DOCKERFILE.parent, SERVICE_DB_IMAGE_NAME, platform=build_platform)
41
+ click.echo("Docker build successful")
42
+
43
+ # Create DB DockerImageArtifact
44
+ click.echo("Creating DB DockerImageArtifact...")
45
+ db_artifact = DockerImageArtifact.put(
46
+ id=f"service-db-{env_id}",
47
+ description=f"ServiceDB PostgreSQL image for {env_id}",
48
+ image_name=SERVICE_DB_IMAGE_NAME,
49
+ )
50
+ click.echo(f"Created DB DockerImageArtifact: id={db_artifact.id} version={db_artifact.version}")
51
+
52
+ # Build db-web image (build instead of pull to avoid docker save manifest issues on Apple Silicon)
53
+ click.echo(f"Building db-web image from {DB_WEB_DOCKERFILE}...")
54
+ build_image(DB_WEB_DOCKERFILE, DB_WEB_DOCKERFILE.parent, DB_WEB_IMAGE_NAME, platform=build_platform)
55
+ click.echo("db-web build successful")
56
+
57
+ # Create db-web DockerImageArtifact
58
+ click.echo("Creating db-web DockerImageArtifact...")
59
+ db_web_artifact = DockerImageArtifact.put(
60
+ id=f"db-web-{env_id}",
61
+ description="db-web lightweight web UI for database inspection",
62
+ image_name=DB_WEB_IMAGE_NAME,
63
+ )
64
+ click.echo(f"Created db-web DockerImageArtifact: id={db_web_artifact.id} version={db_web_artifact.version}")
65
+
66
+ # Build db-mcp image (PostgreSQL MCP server for direct DB access)
67
+ click.echo(f"Building db-mcp image from {DB_MCP_DOCKERFILE}...")
68
+ build_image(DB_MCP_DOCKERFILE, DB_MCP_DOCKERFILE.parent, DB_MCP_IMAGE_NAME, platform=build_platform)
69
+ click.echo("db-mcp build successful")
70
+
71
+ # Create db-mcp DockerImageArtifact
72
+ click.echo("Creating db-mcp DockerImageArtifact...")
73
+ db_mcp_artifact = DockerImageArtifact.put(
74
+ id=f"db-mcp-{env_id}",
75
+ description="db-mcp PostgreSQL MCP server for direct DB access",
76
+ image_name=DB_MCP_IMAGE_NAME,
77
+ )
78
+ click.echo(f"Created db-mcp DockerImageArtifact: id={db_mcp_artifact.id} version={db_mcp_artifact.version}")
79
+
80
+ user_metadata = {}
81
+ for pair in metadata_pairs:
82
+ if "=" not in pair:
83
+ click.echo(f"Invalid metadata format '{pair}', expected key=value", err=True)
84
+ raise click.Abort()
85
+ key, value = pair.split("=", 1)
86
+ user_metadata[key] = value
87
+ metadata = detect_env_metadata(SERVICE_DB_DOCKERFILE, SERVICE_DB_DOCKERFILE.parent)
88
+ metadata.update(user_metadata)
89
+
90
+ # Create ServiceDBEnv
91
+ click.echo("Creating ServiceDBEnv...")
92
+ env = ServiceDBEnv.put(
93
+ id=env_id,
94
+ db_docker_image_artifact=db_artifact,
95
+ db_web_docker_image_artifact=db_web_artifact,
96
+ db_mcp_docker_image_artifact=db_mcp_artifact,
97
+ metadata=metadata if metadata else None,
98
+ )
99
+ click.echo(f"Created ServiceDBEnv: id={env.id} version={env.version}")
@@ -0,0 +1,35 @@
1
+ """Snapshot a deployed MultiEnv's servicedb as a pre-loaded Docker image."""
2
+
3
+ import asyncio
4
+ import logging
5
+
6
+ import click
7
+
8
+ from agent_env.env.snapshot_store import EnvSnapshot
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ async def run_snapshot(instance_id: str) -> EnvSnapshot:
14
+ """Core snapshot logic. Returns the EnvSnapshot.
15
+
16
+ Can be called directly from tests or wrapped by the CLI command.
17
+ """
18
+ return await EnvSnapshot.create(instance_id)
19
+
20
+
21
+ @click.command()
22
+ @click.option("--instance-id", required=True, help="Deployed env instance ID")
23
+ def snapshot(instance_id: str):
24
+ """Snapshot a deployed MultiEnv's servicedb as a pre-loaded Docker image."""
25
+ try:
26
+ result = asyncio.run(run_snapshot(instance_id))
27
+ except ValueError as e:
28
+ click.echo(f"Error: {e}", err=True)
29
+ raise SystemExit(1)
30
+ click.echo(click.style("Snapshot complete!", fg="green"))
31
+ click.echo(f" instance_id: {result.instance_id}")
32
+ click.echo(f" env_id: {result.env_id}")
33
+ click.echo(f" environment_universe_id: {result.environment_universe_id}")
34
+ click.echo(f" is_clean: {result.is_clean}")
35
+ click.echo(f" db_image_artifact: {result.db_image_artifact_id} v{result.db_image_artifact_version}")
@@ -0,0 +1,6 @@
1
+ """Out-of-band lifecycle commands for an env's durable state store."""
2
+
3
+ from .init import init_env_state
4
+ from .teardown import teardown_env_state
5
+
6
+ __all__ = ["init_env_state", "teardown_env_state"]
@@ -0,0 +1,108 @@
1
+ """Pre-initialize a durable remote state store for an env, decoupled from a deploy.
2
+
3
+ Runs the same ``acquire`` + ``prepare`` a deploy would, but out-of-band and with a long TTL, so a
4
+ long-running store can be stood up ahead of time and referenced later by its ``instance_id``
5
+ """
6
+
7
+ import asyncio
8
+
9
+ import click
10
+
11
+ from agent_env.env import Env, MultiEnv
12
+ from agent_env.store.routing import run_scope
13
+
14
+ # A durable store can't be shorter-lived than a normal run, so the floor is the default run TTL
15
+ # (agent-env's current default, mirrored from cli/env/deploy.py).
16
+ MIN_TTL_SECONDS = 10800 # 3 hours (= default run TTL)
17
+ DEFAULT_TTL_SECONDS = 2592000 # 30 days
18
+ MAX_TTL_SECONDS = 31536000 # 365 days
19
+
20
+
21
+ def _resolve_environment_names(env: Env) -> list[str]:
22
+ """One schema per DB-client environment. A MultiEnv contributes its MCP + website environments; a
23
+ standalone servicedb-backed env contributes its single one.
24
+
25
+ Must match the list the gateway passes to ``prepare`` at deploy (``EnvironmentGatewayProvider._all_environments``,
26
+ the authoritative one) — including the ``website_browser`` server the gateway auto-adds whenever an
27
+ env has websites. A name missing here yields a base without that schema, and the deploy then builds
28
+ it a silently-empty overlay."""
29
+ if isinstance(env, MultiEnv):
30
+ names = [e.environment_name for e in env.mcp_server_envs] + [
31
+ e.environment_name for e in env.website_envs
32
+ ]
33
+ if env.website_envs:
34
+ from agent_env.config import get_config
35
+
36
+ names.append(Env.get(get_config().default_website_browser_env_id).environment_name)
37
+ elif hasattr(env, "environment_name"):
38
+ names = [env.environment_name]
39
+ else:
40
+ return []
41
+ return list(dict.fromkeys(names))
42
+
43
+ # TODO: right now this takes an `--id` param because the schemas to initialize the EnvStateInstance with is
44
+ # derived from the env. Refactor this to use environment_cards, and consolidate _resolve_environment_names with the
45
+ # same logic in EnvironmentGatewayProvider._all_environments, so that the code can't drift
46
+ @click.command(name="init-env-state")
47
+ @click.option("--id", "env_id", required=True, help="Env id")
48
+ @click.option("--env-state-type", "env_state_type", default=None,
49
+ help="Env state type to initialize (required). Only external types — those that "
50
+ "allocate a store upstream of the gateway — can be pre-initialized out-of-band. "
51
+ "Which are registered depends on the installed config.toml.")
52
+ @click.option("--ttl-seconds", type=click.IntRange(min=MIN_TTL_SECONDS, max=MAX_TTL_SECONDS),
53
+ default=DEFAULT_TTL_SECONDS,
54
+ help=f"Store lifetime in seconds (min {MIN_TTL_SECONDS}, max {MAX_TTL_SECONDS}, "
55
+ f"default {DEFAULT_TTL_SECONDS} = 30 days)")
56
+ def init_env_state(env_id: str, env_state_type: str | None, ttl_seconds: int):
57
+ """Pre-initialize a durable remote state store for an env and print its instance id."""
58
+ from agent_env.providers.env_state import (
59
+ acquire_state_for_deploy,
60
+ build_state_provider,
61
+ )
62
+
63
+ if not env_state_type:
64
+ raise click.BadParameter("an env state type must be supplied", param_hint="'--env-state-type'")
65
+ state_type = env_state_type
66
+
67
+ try:
68
+ build_state_provider(state_type) # resolve before any Mongo/AWS work, so a typo fails fast
69
+ except ValueError as e:
70
+ raise click.BadParameter(str(e), param_hint="'--env-state-type'") from e
71
+
72
+ click.echo(f"Fetching env: id={env_id}...")
73
+ env = Env.get(env_id)
74
+ click.echo(f"Found env: id={env.id} version={env.version} type={env.type}")
75
+
76
+ environment_names = _resolve_environment_names(env)
77
+ if not environment_names:
78
+ raise click.BadParameter(
79
+ f"env type '{env.type}' has no environments to back with a DB state store",
80
+ param_hint="'--id'",
81
+ )
82
+ click.echo(f"Environments (one schema each): {', '.join(environment_names)}")
83
+
84
+ click.echo(f"Initializing {env_state_type} store (ttl={ttl_seconds}s)...")
85
+
86
+ async def _init() -> "EnvStateInstance | None":
87
+ inst = await acquire_state_for_deploy(
88
+ env_state_type=state_type, ttl_seconds=ttl_seconds, name_hint=env_id,
89
+ )
90
+ if inst is not None:
91
+ await build_state_provider(state_type).prepare(environment_names, instance=inst)
92
+ return inst
93
+
94
+ with run_scope(env.id):
95
+ instance = asyncio.run(_init())
96
+ if instance is None:
97
+ raise click.BadParameter(
98
+ f"env state type '{env_state_type}' cannot be pre-initialized out-of-band",
99
+ param_hint="'--env-state-type'",
100
+ )
101
+
102
+ click.echo("Initialized!")
103
+ click.echo(f" host: {instance.metadata.get('host')}")
104
+ click.echo(f" database: {instance.metadata.get('dbname')}")
105
+ click.echo(f" schemas: {', '.join(environment_names)}")
106
+ click.echo(f" created: {instance.created_at_utc}")
107
+ click.echo(f" expires: {instance.expires_at_utc}")
108
+ click.echo("Env State Instance ID: " + click.style(instance.instance_id, fg="green"))
@@ -0,0 +1,44 @@
1
+ """Tear down a durable env-state store initialized via ``env init-env-state``.
2
+
3
+ Idempotent: tearing down an instance whose store is already gone — or an unknown instance id — logs and no-ops
4
+ rather than erroring, so a future TTL reaper and a manual teardown can race without conflict.
5
+ """
6
+
7
+ import asyncio
8
+
9
+ import click
10
+
11
+
12
+ @click.command(name="teardown-env-state")
13
+ @click.option("--instance-id", "instance_id", required=True, help="Env state instance id (esi-…)")
14
+ def teardown_env_state(instance_id: str):
15
+ """Tear down an env-state store by instance id and retire its record."""
16
+ from agent_env.providers.env_state import build_state_provider, get_env_state_instance_store
17
+ from agent_env.store.base import NotFoundError
18
+
19
+ try:
20
+ instance = get_env_state_instance_store().get(instance_id)
21
+ except NotFoundError:
22
+ click.echo(f"No env state instance {instance_id} found; nothing to tear down.")
23
+ return
24
+
25
+ kind = instance.metadata.get("kind")
26
+ if kind == "base":
27
+ click.secho(
28
+ f"⚠ {instance_id} is a persistent BASE (database "
29
+ f"{instance.metadata.get('dbname')!r}). Tearing it down DROPS the shared base database "
30
+ "and everything materialized in it. Any run overlay still pointing at this base "
31
+ "(base_env_state_instance_id) will be orphaned — its data source disappears.",
32
+ fg="yellow",
33
+ )
34
+ elif kind == "run_overlay":
35
+ click.echo(
36
+ f"{instance_id} is a persistent RUN_OVERLAY over base "
37
+ f"{instance.metadata.get('base_env_state_instance_id')!r}; dropping only its overlay "
38
+ "schemas + run role. The shared base is preserved (not orphaned)."
39
+ )
40
+
41
+ click.echo(f"Tearing down env state instance {instance_id} ({instance.state_type})...")
42
+ provider = build_state_provider(instance.state_type)
43
+ asyncio.run(provider.teardown(instance))
44
+ click.echo("Done. " + click.style(f"{instance_id} torn down / retired", fg="green"))