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,1064 @@
1
+ """Centralized configuration for agent-env.
2
+
3
+ Provides a singleton Config that manages store backends and model routing.
4
+ Configure once at startup, then access via get_config().
5
+
6
+ Backends resolve from ``.agentenv/config.toml`` (or ``AGENT_ENV_CONFIG``); with no
7
+ config, every store defaults to its local backend — no external coordinates are
8
+ built in:
9
+
10
+ - Document store (AGENT_ENV_DOCUMENT_STORE): "local" (or unset): stdlib SQLite at
11
+ document_store/documents.db under the per-user state root (``paths.state_root``).
12
+ MongoDB has no built-in coordinates; configure it via a [stores.document] table.
13
+ - Object store (AGENT_ENV_OBJECT_STORE): "local" (or unset): filesystem under
14
+ object_store/ in the per-user state root. S3 has no built-in coordinates; configure
15
+ it via a [stores.object] table.
16
+ - Image store (AGENT_ENV_IMAGE_STORE): "local" (or unset): an OCI registry at
17
+ localhost:5000. ECR has no built-in coordinates; configure it via a
18
+ [stores.image] table.
19
+ - Secret store (AGENT_ENV_SECRET_STORE): "local" (or unset): process env vars /
20
+ optional local file. AWS Secrets Manager has no built-in coordinates; configure
21
+ it via a [stores.secret] table.
22
+
23
+ AGENT_ENV_FIXTURE_PREFIX (default ""): prepends <prefix>/ to the keys of artifact
24
+ objects, image builds, env and agent snapshots, changelogs, default agent and judge
25
+ trajectories, verifier outputs and the A2A validator's fixtures, so a fresh control-plane
26
+ backend can write without colliding with existing objects in a shared store. Empty in prod.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ import os
33
+ import threading
34
+ from dataclasses import dataclass, field
35
+ from pathlib import Path
36
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional
37
+
38
+ import yaml
39
+
40
+ from agent_env.config import loader as config_loader
41
+ from agent_env.config import model as model_config
42
+ from agent_env.config.errors import ConfigError, EmptySecretBundleError
43
+ from agent_env.config import snapshot as config_snapshot
44
+ from agent_env.config.paths import state_root
45
+ from agent_env.config.provenance import (
46
+ KIND_DEFAULT,
47
+ KIND_ENV,
48
+ KIND_FILE,
49
+ KIND_INSTALLED,
50
+ Layer,
51
+ Traced,
52
+ )
53
+
54
+ if TYPE_CHECKING:
55
+ from pymongo.database import Database
56
+
57
+ from agent_env.store.document_store import DocumentStore, LocalSqliteDocumentStore
58
+ from agent_env.store.image_store import ImageStore
59
+ from agent_env.store.object_store import ObjectStore
60
+ from agent_env.store.secret_store import SecretStore
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ _STORE_BACKEND_MONGO = "mongo"
65
+ _STORE_BACKEND_LOCAL = "local"
66
+
67
+ _OBJECT_BACKEND_S3 = "s3"
68
+ _SECRET_BACKEND_AWS = "aws"
69
+
70
+ _RUNNER_BACKEND_LOCAL = "local"
71
+
72
+ _IMAGE_BACKEND_ECR = "ecr"
73
+ _DEFAULT_LOCAL_REGISTRY_HOST = "localhost:5000"
74
+
75
+
76
+ def _default_sandbox_mode() -> str:
77
+ from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_VM
78
+ return SANDBOX_MODE_VM
79
+
80
+ _HUMAN_A2A_CONFIG_SECTION = "conversations"
81
+ _HUMAN_A2A_CONFIG_KEY = "default_human_a2a_url"
82
+ _AGENTS_CONFIG_SECTION = "agents"
83
+ _AGENTS_CONFIG_KEY = "default_a2a_agent_id"
84
+ _DEFAULT_A2A_AGENT_ID = "a2a-default"
85
+
86
+
87
+ def _validated_human_a2a_url(value: object, source: str) -> str:
88
+ # Fail here rather than as an AttributeError or bogus URL inside a task step.
89
+ if not isinstance(value, str) or not value.strip():
90
+ raise ConfigError(f"{source} must be a non-empty string URL, got {value!r}")
91
+ return value
92
+
93
+
94
+ def _validated_a2a_agent_id(value: object, source: str) -> str:
95
+ if not isinstance(value, str) or not value.strip():
96
+ raise ConfigError(f"{source} must be a non-empty string A2A agent id, got {value!r}")
97
+ return value
98
+
99
+
100
+ def validated_agents_section(section: Mapping[str, Any]) -> Mapping[str, Any]:
101
+ """The ``[agents]`` table, checked as the reader checks it; ``config show`` runs this too."""
102
+ unknown = set(section) - {_AGENTS_CONFIG_KEY}
103
+ if unknown:
104
+ raise ConfigError(
105
+ f"[{_AGENTS_CONFIG_SECTION}] has unknown keys {sorted(unknown)}; "
106
+ f"allowed: ['{_AGENTS_CONFIG_KEY}']"
107
+ )
108
+ if _AGENTS_CONFIG_KEY in section:
109
+ _validated_a2a_agent_id(
110
+ section[_AGENTS_CONFIG_KEY], f"[{_AGENTS_CONFIG_SECTION}] {_AGENTS_CONFIG_KEY}"
111
+ )
112
+ return section
113
+
114
+ @dataclass(frozen=True)
115
+ class ResolvedKey:
116
+ """A single key resolved as ``env > configure() field > config.toml > built-in default``.
117
+
118
+ Declared once in ``RESOLVED_KEYS`` below and read by both the getter and
119
+ ``agent_env.config.describe``, so a report cannot name a layer the getter would not have
120
+ taken — the contract ``AliasedSection`` already carries for whole sections, and the one
121
+ whose absence let a report claim the file while ``configure()`` was winning.
122
+
123
+ The declaration owns *precedence and coordinates* only. Which layers get validated, and
124
+ how, stays with the getter: those rules differ per key and are none of a report's
125
+ business.
126
+ """
127
+
128
+ toml_path: tuple[str, ...]
129
+ env_var: Optional[str] = None
130
+ field: Optional[str] = None
131
+ default: Any = None
132
+ default_label: str = "built-in default"
133
+
134
+ @property
135
+ def name(self) -> str:
136
+ return ".".join(self.toml_path)
137
+
138
+ @property
139
+ def file_label(self) -> str:
140
+ """The spelling the getters' errors use: ``[agents] default_a2a_agent_id``."""
141
+ return f"[{self.toml_path[0]}] {'.'.join(self.toml_path[1:])}"
142
+
143
+
144
+ RESOLVED_KEYS: tuple[ResolvedKey, ...] = (
145
+ ResolvedKey(("model", "api_key"), env_var="LITELLM_API_KEY"),
146
+ ResolvedKey(("model", "base_url"), env_var="LITELLM_BASE_URL"),
147
+ ResolvedKey(("conversations", "default_human_a2a_url"),
148
+ env_var="AGENT_ENV_HUMAN_A2A_URL", field="default_human_a2a_url"),
149
+ ResolvedKey(("agents", "default_a2a_agent_id"), field="default_a2a_agent_id",
150
+ default=_DEFAULT_A2A_AGENT_ID, default_label="built-in default in config.runtime"),
151
+ )
152
+ _KEY_BY_NAME = {key.name: key for key in RESOLVED_KEYS}
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class AliasedSection:
157
+ """A section resolved as ``default < config.toml < env var``, whose value may be written
158
+ as a name that ``alias`` expands into an impl table.
159
+
160
+ Declared once in ``ALIASED_SECTIONS`` below and read by both the resolvers and
161
+ ``agent_env.config.describe``, so a report cannot name coordinates the resolver would
162
+ not have used.
163
+ """
164
+
165
+ name: str
166
+ env_var: str
167
+ toml_path: tuple[str, ...]
168
+ default: str
169
+ alias: Callable[["Config", str], dict]
170
+
171
+
172
+ class PluginFailures:
173
+ """What a Config's registry builds skipped, by group then name, with the reason
174
+ (``agent_env.plugins.load_failures``)."""
175
+
176
+ def __init__(self) -> None:
177
+ self._groups: dict[str, dict[str, str]] = {}
178
+
179
+ def replace(self, group: str, failures: dict[str, str]) -> None:
180
+ """Record ``group``'s skipped plugins; a rebuild of the group replaces its entry."""
181
+ self._groups[group] = failures
182
+
183
+ def reason(self, group: str, name: str) -> Optional[str]:
184
+ return self._groups.get(group, {}).get(name)
185
+
186
+ def snapshot(self) -> dict[str, dict[str, str]]:
187
+ # Copied before reading: another thread may be recording a group it is building.
188
+ groups = dict(self._groups)
189
+ return {group: dict(failures) for group, failures in groups.items() if failures}
190
+
191
+
192
+ # Identity, not field equality: a Config owns a document, so two are interchangeable only if
193
+ # they are the same object. Field equality called two on different config files equal.
194
+ @dataclass(eq=False)
195
+ class Config:
196
+ """Configuration for storage connections."""
197
+
198
+ fixture_prefix: str = field(
199
+ default_factory=lambda: os.getenv("AGENT_ENV_FIXTURE_PREFIX", "")
200
+ )
201
+ agent_sandbox_mode: str = field(
202
+ default_factory=lambda: os.getenv("AGENT_SANDBOX_MODE") or _default_sandbox_mode()
203
+ )
204
+ default_gateway_env_id: str = "default"
205
+ default_service_db_env_id: str = "default-db"
206
+ default_website_browser_env_id: str = "website-browser"
207
+ default_a2a_agent_id: Optional[str] = None
208
+ modal_default_region: str = field(
209
+ default_factory=lambda: os.getenv("AGENT_ENV_MODAL_REGION", "us-east-1")
210
+ )
211
+ default_human_a2a_url: Optional[str] = None
212
+
213
+ _secret: Optional[Mapping[str, Any]] = field(default=None, repr=False, compare=False)
214
+ _snapshot: Optional[config_snapshot.Snapshot] = field(default=None, repr=False, compare=False)
215
+ _snapshot_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
216
+ _document_store: Optional["DocumentStore"] = field(default=None, repr=False, compare=False)
217
+ _document_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
218
+ _image_store: Optional[ImageStore] = field(default=None, repr=False, compare=False)
219
+ _image_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
220
+ _object_store: Optional[ObjectStore] = field(default=None, repr=False, compare=False)
221
+ _object_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
222
+ _local_stores: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
223
+ _routed_stores: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
224
+ _separate_stores: dict[str, tuple[Any, Any]] = field(default_factory=dict, repr=False, compare=False)
225
+ _routing_lock: Any = field(default_factory=threading.RLock, repr=False, compare=False)
226
+ _secret_store: Optional[SecretStore] = field(default=None, repr=False, compare=False)
227
+ _secret_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
228
+ _model_cfg: Optional[model_config.ModelConfig] = field(default=None, repr=False, compare=False)
229
+ _runner: Optional[Any] = field(default=None, repr=False, compare=False)
230
+ _runner_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
231
+ _registries: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
232
+ _plugin_failures: PluginFailures = field(default_factory=PluginFailures, repr=False, compare=False)
233
+
234
+ def _registry(self, name: str, build: Callable[..., Any]) -> Any:
235
+ """One registry, memoized against this Config's document.
236
+
237
+ Unlocked unlike the stores: ``build`` runs ``load_impl``, and racing builds read the
238
+ same pinned document anyway.
239
+ """
240
+ cached = self._registries.get(name)
241
+ if cached is None:
242
+ # Pinned before ``build`` imports any plugin: a plugin package that points
243
+ # AGENT_ENV_CONFIG elsewhere on import must not change which config this one reads.
244
+ self._document()
245
+ state = _registries_building.__dict__
246
+ building = state.setdefault("names", set())
247
+ if name in building:
248
+ # Re-entered while this thread is still building that registry (a plugin asked
249
+ # for it while being imported), so it may lack that plugin: returned, not kept,
250
+ # and its failures unrecorded, since the outer build records the whole group.
251
+ state["nested"] = state.get("nested", 0) + 1
252
+ try:
253
+ return build(self)
254
+ finally:
255
+ state["nested"] -= 1
256
+ building.add(name)
257
+ # A registry first built inside a re-entered one is kept, so it records its own failures.
258
+ outer, state["nested"] = state.get("nested", 0), 0
259
+ try:
260
+ cached = self._registries[name] = build(self)
261
+ finally:
262
+ building.discard(name)
263
+ state["nested"] = outer
264
+ return cached
265
+
266
+ # Imported per method: circular at module scope, and they drag in the model classes.
267
+ def env_registry(self) -> dict[str, Any]:
268
+ from agent_env.env import registry
269
+
270
+ return self._registry("envs", registry._build_registry)
271
+
272
+ def artifact_registry(self) -> dict[str, Any]:
273
+ from agent_env.artifact import registry
274
+
275
+ return self._registry("artifacts", registry._build_registry)
276
+
277
+ def artifact_type_aliases(self) -> dict[str, str]:
278
+ from agent_env.artifact import registry
279
+
280
+ return self._registry("artifact_type_aliases", registry._load_type_aliases)
281
+
282
+ def task_step_registry(self) -> dict[str, Any]:
283
+ from agent_env.task_step import registry
284
+
285
+ return self._registry("task_steps", registry._build_registry)
286
+
287
+ def sandbox_registry(self) -> dict[str, dict]:
288
+ from agent_env.providers.sandbox_providers import sandbox_provider
289
+
290
+ return self._registry("sandbox", sandbox_provider._build_registry)
291
+
292
+ def state_registry(self) -> dict[str, dict]:
293
+ from agent_env.providers.env_state import env_state_provider
294
+
295
+ return self._registry("state", env_state_provider._build_registry)
296
+
297
+ def env_provider_registry(self) -> dict[str, Any]:
298
+ from agent_env.providers.env_providers import env_provider
299
+
300
+ return self._registry("env_providers", env_provider._build_registry)
301
+
302
+ def _get_secret(self) -> Mapping[str, Any]:
303
+ """Combined secret mapping from the configured secret store — the whole-bundle reader
304
+ for callers that index individual keys, sharing ``get_secret_store()`` with ``secret:``
305
+ interpolation (so they can't diverge). Needs a bundle-backed store."""
306
+ if self._secret is None:
307
+ store = self.get_secret_store()
308
+ load = getattr(store, "_load", None)
309
+ if not callable(load):
310
+ raise ConfigError(
311
+ f"{type(store).__name__} exposes no combined secret mapping; "
312
+ "_get_secret() needs a bundle-backed secret store (aws/local)."
313
+ )
314
+ bundle = load()
315
+ if not bundle:
316
+ raise EmptySecretBundleError(
317
+ "Secret bundle is empty: configure a [stores.secret] table (or point "
318
+ "AGENT_ENV_CONFIG at a config.toml with one), or populate the local "
319
+ "secret store's file/values."
320
+ )
321
+ self._secret = bundle
322
+ return self._secret
323
+
324
+ def get_litellm_api_key(self) -> str:
325
+ return self._model_config().resolve_api_key(secret_resolver=self._resolve_secret)
326
+
327
+ def get_modal_credentials(self) -> tuple[str, str]:
328
+ token_id = os.getenv("MODAL_TOKEN_ID")
329
+ token_secret = os.getenv("MODAL_TOKEN_SECRET")
330
+ if token_id and token_secret:
331
+ return token_id, token_secret
332
+
333
+ modal_toml = Path.home() / ".modal.toml"
334
+ if modal_toml.exists():
335
+ import tomllib
336
+ for profile in tomllib.loads(modal_toml.read_text()).values():
337
+ if isinstance(profile, dict) and profile.get("active") and profile.get("token_id") and profile.get("token_secret"):
338
+ return profile["token_id"], profile["token_secret"]
339
+
340
+ secret = self._get_secret()
341
+ return secret["modal_token_id"], secret["modal_token_secret"]
342
+
343
+ def get_litellm_base_url(self) -> str:
344
+ base = os.getenv("LITELLM_BASE_URL") or self._model_config().base_url
345
+ if not base:
346
+ raise ConfigError(
347
+ "No model endpoint configured: set [model] base_url in "
348
+ ".agentenv/config.toml or the LITELLM_BASE_URL env var."
349
+ )
350
+ return base
351
+
352
+ def _model_config(self) -> model_config.ModelConfig:
353
+ """The parsed ``[model]`` config, cached."""
354
+ if self._model_cfg is None:
355
+ self._model_cfg = model_config.ModelConfig.from_section(
356
+ self.section("model"), secret_resolver=self._resolve_secret
357
+ )
358
+ return self._model_cfg
359
+
360
+ def get_default_human_a2a_url(self) -> str:
361
+ """The base URL a human-A2A (HITL) step parks its conversation on.
362
+
363
+ First match wins: ``AGENT_ENV_HUMAN_A2A_URL`` (absolute — local dev repoints
364
+ it at the local hub), an explicit ``configure(default_human_a2a_url=...)``,
365
+ then config.toml ``[conversations] default_human_a2a_url``; unconfigured
366
+ resolution raises, so call this only where the URL is consumed. Returns the
367
+ *base* URL; ``deploy_human_agent`` appends ``/instance/{instance_id}``.
368
+ """
369
+ key = _KEY_BY_NAME[f"{_HUMAN_A2A_CONFIG_SECTION}.{_HUMAN_A2A_CONFIG_KEY}"]
370
+ for layer in self.key_candidates(key):
371
+ if layer.kind == KIND_ENV:
372
+ if layer.raw:
373
+ return layer.raw # deliberately unvalidated, as before
374
+ continue
375
+ if layer.kind == KIND_INSTALLED:
376
+ return _validated_human_a2a_url(layer.raw, key.field)
377
+ # Only here, where the file is what we are about to take, does its shape matter.
378
+ # Checking it earlier lets a malformed [conversations] beat an override that was
379
+ # never going to read the file at all.
380
+ self.section(_HUMAN_A2A_CONFIG_SECTION)
381
+ resolved = config_loader.interpolate(layer.raw, secret_resolver=self._resolve_secret)
382
+ return _validated_human_a2a_url(resolved, key.file_label)
383
+ raise ConfigError(f"No human-A2A URL configured: set [{_HUMAN_A2A_CONFIG_SECTION}] {_HUMAN_A2A_CONFIG_KEY} or AGENT_ENV_HUMAN_A2A_URL")
384
+
385
+ def get_default_a2a_agent_id(self) -> str:
386
+ """The agent a ``deploy_agent`` step without an id deploys:
387
+ ``configure(default_a2a_agent_id=...)`` > ``[agents] default_a2a_agent_id`` > ``a2a-default``."""
388
+ key = _KEY_BY_NAME[f"{_AGENTS_CONFIG_SECTION}.{_AGENTS_CONFIG_KEY}"]
389
+ for layer in self.key_candidates(key):
390
+ if layer.kind == KIND_INSTALLED:
391
+ return _validated_a2a_agent_id(layer.raw, key.field)
392
+ # The file is in play now — or the built-in default beneath it, which the old
393
+ # code also validated the section before returning. Not before: a broken table
394
+ # must not beat configure().
395
+ validated_agents_section(self.section(_AGENTS_CONFIG_SECTION))
396
+ if layer.kind == KIND_DEFAULT:
397
+ return layer.raw
398
+ resolved = config_loader.interpolate(layer.raw, secret_resolver=self._resolve_secret)
399
+ return _validated_a2a_agent_id(resolved, key.file_label)
400
+ return _DEFAULT_A2A_AGENT_ID
401
+
402
+ def get_default_model(self) -> str | None:
403
+ return self._model_config().default
404
+
405
+ def get_model_for_role(self, role: str) -> str | None:
406
+ return self._model_config().model_for_role(role)
407
+
408
+ def get_model_params(self, overrides: dict | None = None) -> dict:
409
+ params = dict(self._model_config().params)
410
+ if overrides:
411
+ params.update(config_loader.interpolate(overrides, secret_resolver=self._resolve_secret))
412
+ return params
413
+
414
+ def resolve_model_call(
415
+ self,
416
+ model: str,
417
+ *,
418
+ default_api_key: str | None = None,
419
+ base_override: str | None = None,
420
+ model_overrides: dict[str, dict[str, str]] | None = None,
421
+ ) -> model_config.ModelCallConfig:
422
+ return self._model_config().resolve_call(
423
+ model,
424
+ default_api_key=default_api_key,
425
+ base_override=base_override,
426
+ model_overrides=model_overrides,
427
+ secret_resolver=self._resolve_secret,
428
+ )
429
+
430
+ @property
431
+ def db(self) -> "Database":
432
+ """Raw pymongo Database shared with the configured Mongo document store, for callers
433
+ that need collections the DocumentStore API does not model."""
434
+ from agent_env.store.document_store import MongoDocumentStore
435
+
436
+ store = self._configured_document_store()
437
+ if not isinstance(store, MongoDocumentStore):
438
+ raise ConfigError(
439
+ "Config.db needs a Mongo document store, but the configured store is "
440
+ f"{type(store).__name__}: configure a [stores.document] table with a "
441
+ "MongoDocumentStore impl (and unset AGENT_ENV_DOCUMENT_STORE, which "
442
+ "overrides it)."
443
+ )
444
+ return store.database
445
+
446
+ def get_document_store(self) -> "DocumentStore":
447
+ """The control-plane document store (cached): an explicitly-set backend, else the one from
448
+ ``AGENT_ENV_DOCUMENT_STORE`` / config.toml (``local`` → SQLite default). With namespace
449
+ routing on, ``@local`` documents and ``@local`` runs' records go to the ``@local`` namespace's
450
+ own local store (``agent_env.store.routing``)."""
451
+ from agent_env.store import routing
452
+
453
+ configured = self._configured_document_store()
454
+ if not routing.namespace_routing_enabled():
455
+ return configured
456
+ return self._routed("document", configured)
457
+
458
+ def _configured_document_store(self) -> "DocumentStore":
459
+ if self._document_store is None:
460
+ with self._document_store_lock:
461
+ if self._document_store is None:
462
+ self._document_store = self._build_default_document_store()
463
+ return self._document_store
464
+
465
+ def set_document_store(self, store: "DocumentStore") -> None:
466
+ """Install an explicit backend; get_document_store() then returns it verbatim."""
467
+ self._document_store = store
468
+
469
+ def _build_default_document_store(self) -> DocumentStore:
470
+ from agent_env.store.document_store import DocumentStore
471
+
472
+ section = self._resolve_document_section()
473
+ return config_loader.build_store(section, DocumentStore, secret_resolver=self._resolve_secret)
474
+
475
+ def trace_section(self, name: str) -> Traced:
476
+ """A declared section's value plus every layer that could have supplied it — winner
477
+ first, later layer wins: default < config.toml < env var.
478
+
479
+ A string (an env value or a bare toml value) expands through the section's alias; a
480
+ table is used as-is. Reporting reads the same layers, so both the precedence rule and
481
+ the section's coordinates are stated exactly once.
482
+ """
483
+ section = _BY_NAME[name]
484
+ candidates = self.section_candidates(name)
485
+ if candidates[0].error is not None:
486
+ raise ConfigError(candidates[0].error)
487
+ raw = candidates[0].raw
488
+ return Traced(
489
+ value=section.alias(self, raw) if isinstance(raw, str) else raw,
490
+ winner=candidates[0],
491
+ shadowed=tuple(candidates[1:]),
492
+ )
493
+
494
+ def key_candidates(self, key: "ResolvedKey") -> list[Layer]:
495
+ """Every layer that could supply ``key``, highest priority first.
496
+
497
+ Separate from the getters so the report reads the same order they take, rather than
498
+ a second copy of it that can drift.
499
+ """
500
+ candidates: list[Layer] = []
501
+ if key.env_var is not None:
502
+ override = os.getenv(key.env_var)
503
+ if override is not None:
504
+ candidates.append(Layer(KIND_ENV, f"${key.env_var}", override))
505
+ if key.field is not None:
506
+ installed = getattr(self, key.field, None)
507
+ if installed is not None:
508
+ candidates.append(Layer(KIND_INSTALLED, f"configure({key.field}=...)", installed))
509
+ from_file = self.file_candidate(key.toml_path)
510
+ if from_file is not None:
511
+ candidates.append(from_file)
512
+ if key.default is not None:
513
+ candidates.append(Layer(KIND_DEFAULT, key.default_label, key.default))
514
+ return candidates
515
+
516
+ def section_candidates(self, name: str) -> list[Layer]:
517
+ """Every layer that could supply a declared section, highest priority first.
518
+
519
+ Separate from resolution so a report can still name the layer at fault when turning
520
+ that layer's value into a section is what failed.
521
+ """
522
+ section = _BY_NAME[name]
523
+ candidates: list[Layer] = []
524
+ override = os.getenv(section.env_var)
525
+ if override is not None:
526
+ candidates.append(Layer(KIND_ENV, f"${section.env_var}", override))
527
+ from_file = self.file_candidate(section.toml_path)
528
+ if from_file is not None:
529
+ candidates.append(from_file)
530
+ candidates.append(Layer(KIND_DEFAULT, "built-in default", section.default))
531
+ return candidates
532
+
533
+ def file_candidate(self, toml_path: tuple[str, ...]) -> Optional[Layer]:
534
+ """The file's contribution to this section, or a candidate carrying why it could not
535
+ be read. Never raises: a layer that something outranks must not be able to break the
536
+ layer that outranks it — `trace_section` raises only if this one wins.
537
+ """
538
+ label = f"[{'.'.join(toml_path)}]"
539
+ try:
540
+ node: Any = self.config_file()
541
+ except (ConfigError, OSError) as e:
542
+ return Layer(KIND_FILE, label, None, error=str(e))
543
+ walked: list[str] = []
544
+ for part in toml_path:
545
+ if not isinstance(node, dict):
546
+ return Layer(KIND_FILE, label, None, error=(
547
+ f"config.toml [{'.'.join(walked)}] must be a table, got {type(node).__name__}"
548
+ ))
549
+ walked.append(part)
550
+ node = node.get(part)
551
+ if node is None:
552
+ return None
553
+ return Layer(KIND_FILE, label, node)
554
+
555
+ def _resolve_document_section(self) -> dict:
556
+ return self.trace_section("document").value
557
+
558
+ def _document_alias(self, name: str) -> dict:
559
+ if name == _STORE_BACKEND_MONGO:
560
+ raise ConfigError(
561
+ "The 'mongo' document backend has no built-in coordinates: configure a "
562
+ "[stores.document] table (and unset AGENT_ENV_DOCUMENT_STORE, which "
563
+ "overrides it)."
564
+ )
565
+ if name == _STORE_BACKEND_LOCAL:
566
+ return {
567
+ "impl": "agent_env.store.document_store:LocalSqliteDocumentStore",
568
+ "config": {"path": self._local_document_store_path()},
569
+ }
570
+ raise ConfigError(
571
+ f"Unknown AGENT_ENV_DOCUMENT_STORE={name!r} (expected 'local', or a [stores.document] table for mongo)"
572
+ )
573
+
574
+ def _local_document_store_path(self) -> str:
575
+ return str(state_root() / "document_store" / "documents.db")
576
+
577
+ def local_namespace_document_store(self) -> "LocalSqliteDocumentStore":
578
+ """The ``@local`` namespace's own document store, whether or not routing is on. Records about
579
+ ``@local`` entities, like the bundle ledger, live beside them here and never reach a configured
580
+ store. Building it creates nothing; its first write creates the file."""
581
+ return self._local_store("document")
582
+
583
+ def _local_namespace_document_store_path(self) -> str:
584
+ return str(state_root() / "document_store" / "local.db")
585
+
586
+ def _document(self) -> config_snapshot.Snapshot:
587
+ """The document this Config reads, resolved on first use and kept.
588
+
589
+ Lazy, not in ``__init__``: ``config show`` builds a Config to *report* a broken
590
+ ``AGENT_ENV_CONFIG``, and a raising constructor would take that report away.
591
+ """
592
+ if self._snapshot is None:
593
+ with self._snapshot_lock:
594
+ if self._snapshot is None:
595
+ self._snapshot = config_snapshot.resolve()
596
+ return self._snapshot
597
+
598
+ def section(self, *names: str) -> Mapping[str, Any]:
599
+ """The config table at ``names``: ``{}`` when absent, an error when mis-shaped."""
600
+ return self._document().section(*names)
601
+
602
+ def config_path(self) -> Optional[Path]:
603
+ """The config.toml this Config resolves against. The process resolves one document,
604
+ so every section and every local-store path agree about which file they came from."""
605
+ return self._document().path
606
+
607
+ def config_file(self) -> dict:
608
+ """The parsed config.toml (``{}`` when absent) — this Config's one resolution.
609
+
610
+ Raises if that resolution failed to parse. The path stays available through
611
+ ``config_path`` either way, which is what lets a report name a file it cannot read.
612
+ """
613
+ return dict(self._document().section())
614
+
615
+ def get_object_store(self) -> ObjectStore:
616
+ """The object (blob) store (cached): an explicitly-set backend, else the one from
617
+ ``AGENT_ENV_OBJECT_STORE`` / config.toml (``local`` → filesystem default). While an ``@local``
618
+ task runs under namespace routing, its view of it keeps writes local."""
619
+ from agent_env.store import routing
620
+
621
+ configured = self._configured_object_store()
622
+ if not routing.in_local_run():
623
+ return configured
624
+ return self._routed("object", configured)
625
+
626
+ def get_object_store_for(self, entity_id: str) -> ObjectStore:
627
+ """The object store ``entity_id``'s objects are written to: under namespace routing, the
628
+ per-user local one for an ``@local`` id; otherwise the configured one."""
629
+ from agent_env.store import routing
630
+ from agent_env.store.ids import is_local_id
631
+
632
+ configured, local = self._namespace_stores("object")
633
+ if local is None:
634
+ return configured
635
+ if is_local_id(entity_id):
636
+ return local
637
+ routing.refuse_in_local_run(f"writing objects for {entity_id!r}")
638
+ return configured
639
+
640
+ def get_object_store_at(self, object_url: str) -> ObjectStore:
641
+ """The object store ``object_url`` addresses: under namespace routing a url the local store
642
+ owns is the local store's, unless the configured store owns it too."""
643
+ from agent_env.store import routing
644
+
645
+ configured, local = self._namespace_stores("object")
646
+ return configured if local is None else routing.object_store_holding(object_url, configured, local)
647
+
648
+ def get_object_store_to_write(self, object_url: str, entity_id: str) -> ObjectStore:
649
+ """``get_object_store_at`` for writing, or registering, ``entity_id``'s object at ``object_url``."""
650
+ self.check_object_url(entity_id, object_url)
651
+ return self.get_object_store_at(object_url)
652
+
653
+ def check_object_url(self, entity_id: str, object_url: str) -> None:
654
+ """Refuse recording ``object_url`` as ``entity_id``'s object across namespaces: under namespace
655
+ routing an ``@local`` entity's objects live in the local store, a bare one's in the configured
656
+ store, and an ``@local`` task run writes only locally."""
657
+ from agent_env.store import routing
658
+ from agent_env.store.ids import is_local_id
659
+
660
+ configured, local = self._namespace_stores("object")
661
+ if local is None:
662
+ return
663
+ store = routing.object_store_holding(object_url, configured, local)
664
+ if is_local_id(entity_id) and store is not local:
665
+ raise ValueError(f"{entity_id!r} is an @local id, so its objects go to the local object store, not {object_url!r}")
666
+ if not is_local_id(entity_id) and store is local:
667
+ raise ValueError(f"{entity_id!r} is not an @local id, so its objects go to the configured object store, not {object_url!r}")
668
+ if store is configured:
669
+ routing.refuse_in_local_run(f"a write to {object_url!r}")
670
+
671
+ def check_local_run_write(self, entity_id: str) -> None:
672
+ """Refuse writing bare-id ``entity_id`` while an ``@local`` task runs where a configured store
673
+ isn't the per-user one: the write would land in a shared store."""
674
+ from agent_env.store import routing
675
+ from agent_env.store.ids import is_local_id
676
+
677
+ if not routing.in_local_run() or is_local_id(entity_id):
678
+ return
679
+ if any(self._namespace_stores(kind)[1] is not None for kind in ("document", "object", "image")):
680
+ routing.refuse_in_local_run(f"writing {entity_id!r}")
681
+
682
+ def _configured_object_store(self) -> ObjectStore:
683
+ if self._object_store is None:
684
+ with self._object_store_lock:
685
+ if self._object_store is None:
686
+ self._object_store = self._build_default_object_store()
687
+ return self._object_store
688
+
689
+ def set_object_store(self, store: ObjectStore) -> None:
690
+ """Install an explicit backend; get_object_store() then returns it verbatim."""
691
+ self._object_store = store
692
+
693
+ def _build_default_object_store(self) -> ObjectStore:
694
+ from agent_env.store.object_store import ObjectStore
695
+
696
+ section = self._resolve_object_section()
697
+ return config_loader.build_store(section, ObjectStore, secret_resolver=self._resolve_secret)
698
+
699
+ def _resolve_object_section(self) -> dict:
700
+ return self.trace_section("object").value
701
+
702
+ def _object_alias(self, name: str) -> dict:
703
+ if name == _OBJECT_BACKEND_S3:
704
+ raise ConfigError(
705
+ "The 's3' object backend has no built-in coordinates: configure a "
706
+ "[stores.object] table (and unset AGENT_ENV_OBJECT_STORE, which overrides it)."
707
+ )
708
+ if name == _STORE_BACKEND_LOCAL:
709
+ return {
710
+ "impl": "agent_env.store.object_store:LocalFilesystemObjectStore",
711
+ "config": {"root": self._local_object_store_path()},
712
+ }
713
+ raise ConfigError(
714
+ f"Unknown AGENT_ENV_OBJECT_STORE={name!r} (expected 'local', or a [stores.object] table for a hosted backend)"
715
+ )
716
+
717
+ def _local_object_store_path(self) -> str:
718
+ return str(state_root() / "object_store")
719
+
720
+ def get_image_store(self) -> ImageStore:
721
+ """The image (registry) store (cached): an explicitly-set backend, else the one from
722
+ ``AGENT_ENV_IMAGE_STORE`` / config.toml (``local`` → local registry default). While an
723
+ ``@local`` task runs under namespace routing, its view of it keeps pushes local."""
724
+ from agent_env.store import routing
725
+
726
+ configured = self._configured_image_store()
727
+ if not routing.in_local_run():
728
+ return configured
729
+ return self._routed("image", configured)
730
+
731
+ def get_image_store_for(self, entity_id: str) -> ImageStore:
732
+ """The image store ``entity_id``'s image is pushed to: under namespace routing, the local
733
+ registry for an ``@local`` id; otherwise the configured one."""
734
+ from agent_env.store import routing
735
+ from agent_env.store.ids import is_local_id
736
+
737
+ configured, local = self._namespace_stores("image")
738
+ if local is None:
739
+ return configured
740
+ if is_local_id(entity_id):
741
+ return local
742
+ routing.refuse_in_local_run(f"pushing an image for {entity_id!r}")
743
+ return configured
744
+
745
+ def get_image_store_at(self, ref: str) -> ImageStore:
746
+ """The image store serving ``ref``: under namespace routing, the local registry for a ref on
747
+ its host; otherwise the configured one."""
748
+ from agent_env.store import routing
749
+
750
+ configured, local = self._namespace_stores("image")
751
+ return configured if local is None else routing.image_store_holding(ref, configured, local)
752
+
753
+ def _configured_image_store(self) -> ImageStore:
754
+ if self._image_store is None:
755
+ with self._image_store_lock:
756
+ if self._image_store is None:
757
+ self._image_store = self._build_default_image_store()
758
+ return self._image_store
759
+
760
+ def set_image_store(self, store: ImageStore) -> None:
761
+ """Install an explicit backend; get_image_store() then returns it verbatim."""
762
+ self._image_store = store
763
+
764
+ def _build_default_image_store(self) -> ImageStore:
765
+ from agent_env.store.image_store import ImageStore
766
+
767
+ section = self._resolve_image_section()
768
+ return config_loader.build_store(section, ImageStore, secret_resolver=self._resolve_secret)
769
+
770
+ def _resolve_image_section(self) -> dict:
771
+ return self.trace_section("image").value
772
+
773
+ def _image_alias(self, name: str) -> dict:
774
+ if name == _IMAGE_BACKEND_ECR:
775
+ raise ConfigError(
776
+ "The 'ecr' image backend has no built-in coordinates: configure a "
777
+ "[stores.image] table (and unset AGENT_ENV_IMAGE_STORE, which overrides it)."
778
+ )
779
+ if name == _STORE_BACKEND_LOCAL:
780
+ return {
781
+ "impl": "agent_env.store.image_store:LocalRegistryImageStore",
782
+ "config": {"registry_host": _DEFAULT_LOCAL_REGISTRY_HOST},
783
+ }
784
+ raise ConfigError(
785
+ f"Unknown AGENT_ENV_IMAGE_STORE={name!r} (expected 'local', or a [stores.image] table for ecr)"
786
+ )
787
+
788
+ def _local_store(self, kind: str) -> Any:
789
+ """The store of ``kind`` the ``@local`` namespace lives in. Documents get a file of their own
790
+ beside the default local store's, so neither ever holds the other's documents; objects and
791
+ images use the per-user object store and local registry, whose urls and repositories already
792
+ say whose they are."""
793
+ from agent_env.store.document_store import DocumentStore
794
+ from agent_env.store.image_store import ImageStore
795
+ from agent_env.store.object_store import ObjectStore
796
+
797
+ with self._routing_lock:
798
+ if kind not in self._local_stores:
799
+ if kind == "document":
800
+ section, abc = {
801
+ "impl": "agent_env.store.routing:LocalNamespaceDocumentStore",
802
+ "config": {"path": self._local_namespace_document_store_path()},
803
+ }, DocumentStore
804
+ else:
805
+ alias, abc = {"object": (self._object_alias, ObjectStore), "image": (self._image_alias, ImageStore)}[kind]
806
+ section = alias(_STORE_BACKEND_LOCAL)
807
+ self._local_stores[kind] = config_loader.build_store(section, abc, secret_resolver=self._resolve_secret)
808
+ return self._local_stores[kind]
809
+
810
+ def _namespace_stores(self, kind: str) -> tuple[Any, Any]:
811
+ """The configured store of ``kind`` and, under namespace routing, the ``@local`` namespace's
812
+ store when it is a separate one (else None)."""
813
+ from agent_env.store import routing
814
+
815
+ configured = {"document": self._configured_document_store, "object": self._configured_object_store,
816
+ "image": self._configured_image_store}[kind]()
817
+ local = self._separate_local_store(kind, configured) if routing.namespace_routing_enabled() else None
818
+ return configured, local
819
+
820
+ def _separate_local_store(self, kind: str, configured: Any) -> Any:
821
+ """The ``@local`` namespace's store of ``kind``, or None when ``configured`` already is it.
822
+ Decided once per configured store: every store access asks."""
823
+ cached = self._separate_stores.get(kind)
824
+ if cached is not None and cached[0] is configured:
825
+ return cached[1]
826
+ local = self._compare_local_store(kind, configured)
827
+ self._separate_stores[kind] = (configured, local)
828
+ return local
829
+
830
+ def _compare_local_store(self, kind: str, configured: Any) -> Any:
831
+ from agent_env.store.document_store import LocalSqliteDocumentStore
832
+ from agent_env.store.image_store import LocalRegistryImageStore
833
+ from agent_env.store.image_store.oci_registry_credentials import normalize_registry_host
834
+ from agent_env.store.object_store import LocalFilesystemObjectStore
835
+
836
+ local = self._local_store(kind)
837
+ if kind == "document":
838
+ same = isinstance(configured, LocalSqliteDocumentStore) and configured.path.resolve() == local.path.resolve()
839
+ elif kind == "object":
840
+ same = isinstance(configured, LocalFilesystemObjectStore) and configured.root.resolve() == local.root.resolve()
841
+ else:
842
+ same = isinstance(configured, LocalRegistryImageStore) and (
843
+ normalize_registry_host(configured.registry_host) == normalize_registry_host(local.registry_host)
844
+ )
845
+ return None if same else local
846
+
847
+ def _routed(self, kind: str, configured: Any) -> Any:
848
+ """``configured`` wrapped by namespace routing, or ``configured`` itself when it already is
849
+ the ``@local`` namespace's store."""
850
+ from agent_env.store import routing
851
+
852
+ local = self._separate_local_store(kind, configured)
853
+ if local is None:
854
+ if kind == "document":
855
+ raise ConfigError(
856
+ f"{self._local_namespace_document_store_path()} is kept for @local documents; "
857
+ "point [stores.document] somewhere else"
858
+ )
859
+ return configured
860
+ with self._routing_lock:
861
+ routed = self._routed_stores.get(kind)
862
+ if routed is None or routed.configured is not configured:
863
+ if kind == "document":
864
+ routed = routing.RoutingDocumentStore(configured, local, local.path)
865
+ elif kind == "object":
866
+ routed = routing.LocalRunObjectStore(configured, local)
867
+ else:
868
+ routed = routing.LocalRunImageStore(configured, local)
869
+ self._routed_stores[kind] = routed
870
+ return routed
871
+
872
+ def get_runner(self):
873
+ """The configured Runner (cached): an explicitly-set one, else ``[runner]`` /
874
+ ``AGENT_ENV_RUNNER`` (``local`` default -> the in-process asyncio runner)."""
875
+ if self._runner is None:
876
+ with self._runner_lock:
877
+ if self._runner is None:
878
+ self._runner = self._build_default_runner()
879
+ return self._runner
880
+
881
+ def set_runner(self, runner) -> None:
882
+ """Install an explicit runner; get_runner() then returns it verbatim."""
883
+ self._runner = runner
884
+
885
+ def _build_default_runner(self):
886
+ from agent_env.config import loader as _loader
887
+ from agent_env.runner.runner import Runner
888
+
889
+ section = self._resolve_runner_section()
890
+ return _loader.build_store(section, Runner, secret_resolver=self._resolve_secret)
891
+
892
+ def _resolve_runner_section(self) -> dict:
893
+ return self.trace_section("runner").value
894
+
895
+ def _runner_alias(self, name: str) -> dict:
896
+ if name == _RUNNER_BACKEND_LOCAL:
897
+ return {"impl": "agent_env.runner.local_runner:LocalRunner", "config": {}}
898
+ raise ConfigError(f"Unknown AGENT_ENV_RUNNER={name!r} (expected 'local', or a [runner] table)")
899
+
900
+ def get_secret_store(self) -> SecretStore:
901
+ """The secret store (cached) that ``secret:`` references resolve through: an explicitly-set
902
+ backend, else ``AGENT_ENV_SECRET_STORE`` / config.toml (``local`` default → env/file)."""
903
+ if self._secret_store is None:
904
+ with self._secret_store_lock:
905
+ if self._secret_store is None:
906
+ self._secret_store = self._build_default_secret_store()
907
+ return self._secret_store
908
+
909
+ def _resolve_secret(self, name: str) -> Optional[str]:
910
+ """Bound so the secret store is built only when a ``secret:`` reference is actually met."""
911
+ return self.get_secret_store().get(name)
912
+
913
+ def set_secret_store(self, store: SecretStore) -> None:
914
+ """Install an explicit backend; get_secret_store() then returns it verbatim."""
915
+ self._secret_store = store
916
+
917
+ def _build_default_secret_store(self) -> SecretStore:
918
+ from agent_env.store.secret_store import SecretStore
919
+
920
+ section = self._resolve_secret_section()
921
+ return config_loader.build_store(section, SecretStore)
922
+
923
+ def _resolve_secret_section(self) -> dict:
924
+ return self.trace_section("secret").value
925
+
926
+ def _secret_alias(self, name: str) -> dict:
927
+ if name == _SECRET_BACKEND_AWS:
928
+ raise ConfigError(
929
+ "The 'aws' secret backend has no built-in coordinates: configure a [stores.secret] "
930
+ "table (and unset AGENT_ENV_SECRET_STORE, which overrides it)."
931
+ )
932
+ if name == _STORE_BACKEND_LOCAL:
933
+ return {"impl": "agent_env.store.secret_store:LocalSecretStore", "config": {}}
934
+ raise ConfigError(
935
+ f"Unknown AGENT_ENV_SECRET_STORE={name!r} (expected 'local', or a [stores.secret] table for a hosted backend)"
936
+ )
937
+
938
+ def get_artifact_key_prefix(self) -> str:
939
+ """Key prefix for every object key core builds: artifacts, image builds, env and agent
940
+ snapshots, changelogs, default trajectories, verifier outputs and validator fixtures (set
941
+ via AGENT_ENV_FIXTURE_PREFIX; empty in prod)."""
942
+ return f"{self.fixture_prefix}/" if self.fixture_prefix else ""
943
+
944
+
945
+ # One row per aliased section, carrying every coordinate that section has. The resolvers and
946
+ # the report both read this, so neither can drift from the other. The alias is held as a
947
+ # function object rather than a method name: a rename then fails at import, not at use.
948
+ ALIASED_SECTIONS = (
949
+ AliasedSection("document", "AGENT_ENV_DOCUMENT_STORE", ("stores", "document"),
950
+ _STORE_BACKEND_LOCAL, Config._document_alias),
951
+ AliasedSection("object", "AGENT_ENV_OBJECT_STORE", ("stores", "object"),
952
+ _STORE_BACKEND_LOCAL, Config._object_alias),
953
+ AliasedSection("image", "AGENT_ENV_IMAGE_STORE", ("stores", "image"),
954
+ _STORE_BACKEND_LOCAL, Config._image_alias),
955
+ AliasedSection("secret", "AGENT_ENV_SECRET_STORE", ("stores", "secret"),
956
+ _STORE_BACKEND_LOCAL, Config._secret_alias),
957
+ AliasedSection("runner", "AGENT_ENV_RUNNER", ("runner",),
958
+ _RUNNER_BACKEND_LOCAL, Config._runner_alias),
959
+ )
960
+ _BY_NAME = {section.name: section for section in ALIASED_SECTIONS}
961
+
962
+ _config: Optional[Config] = None
963
+ _config_lock = threading.Lock()
964
+ # Registry names this thread is building, on any Config; see Config._registry.
965
+ _registries_building = threading.local()
966
+
967
+
968
+ def in_nested_registry_build() -> bool:
969
+ """Whether this thread is inside a re-entered, unmemoized registry build."""
970
+ return _registries_building.__dict__.get("nested", 0) > 0
971
+
972
+
973
+ def configure(
974
+ *,
975
+ default_human_a2a_url: Optional[str] = None,
976
+ default_a2a_agent_id: Optional[str] = None,
977
+ document_store: Optional[DocumentStore] = None,
978
+ image_store: Optional[ImageStore] = None,
979
+ object_store: Optional[ObjectStore] = None,
980
+ secret_store: Optional[SecretStore] = None,
981
+ ) -> None:
982
+ """Rebuild the process-wide config from the config file ``AGENT_ENV_CONFIG`` resolves to.
983
+
984
+ Pass ``document_store`` / ``image_store`` / ``object_store`` / ``secret_store`` to install explicit backends on the new config.
985
+
986
+ ``default_human_a2a_url`` overrides config.toml ``[conversations]`` but not
987
+ ``AGENT_ENV_HUMAN_A2A_URL``. ``default_a2a_agent_id`` overrides config.toml ``[agents]``.
988
+
989
+ Select the stage *before* calling this, by pointing ``AGENT_ENV_CONFIG`` at a per-stage
990
+ config file (a plugin can do this for you, for instance from a CLI root option it registers).
991
+ """
992
+ global _config
993
+ overrides: dict[str, Any] = {}
994
+ if default_human_a2a_url is not None:
995
+ overrides["default_human_a2a_url"] = default_human_a2a_url
996
+ if default_a2a_agent_id is not None:
997
+ overrides["default_a2a_agent_id"] = default_a2a_agent_id
998
+ config_path = config_loader.discover_config_path()
999
+ _config = Config(**overrides)
1000
+ if document_store is not None:
1001
+ _config.set_document_store(document_store)
1002
+ if image_store is not None:
1003
+ _config.set_image_store(image_store)
1004
+ if object_store is not None:
1005
+ _config.set_object_store(object_store)
1006
+ if secret_store is not None:
1007
+ _config.set_secret_store(secret_store)
1008
+ logger.info("Store config resolved: %s", config_path or "(no config file)")
1009
+
1010
+
1011
+ def set_document_store(store: DocumentStore) -> None:
1012
+ """Override the process-wide control-plane document store backend."""
1013
+ get_config().set_document_store(store)
1014
+
1015
+
1016
+ def set_image_store(store: ImageStore) -> None:
1017
+ """Override the process-wide image (registry) store backend."""
1018
+ get_config().set_image_store(store)
1019
+
1020
+
1021
+ def set_object_store(store: ObjectStore) -> None:
1022
+ """Override the process-wide object (blob) store backend."""
1023
+ get_config().set_object_store(store)
1024
+
1025
+
1026
+ def set_secret_store(store: SecretStore) -> None:
1027
+ """Override the process-wide secret store backend."""
1028
+ get_config().set_secret_store(store)
1029
+
1030
+
1031
+ def set_runner(runner) -> None:
1032
+ """Install an explicit Runner on the singleton config."""
1033
+ get_config().set_runner(runner)
1034
+
1035
+
1036
+ def get_runner():
1037
+ """The configured Runner (see ``[runner]`` in .agentenv/config.toml)."""
1038
+ return get_config().get_runner()
1039
+
1040
+
1041
+ def get_config() -> Config:
1042
+ """The process-wide Config, built on first use.
1043
+
1044
+ Locked: the Config owns a document now, so a losing racer's — and anything built from
1045
+ it — would be discarded while its caller kept using it.
1046
+ """
1047
+ global _config
1048
+ cached = _config
1049
+ if cached is not None:
1050
+ return cached
1051
+ with _config_lock:
1052
+ if _config is None:
1053
+ _config = Config()
1054
+ return _config
1055
+
1056
+
1057
+ def reset_config() -> None:
1058
+ """Drop the process-wide config, so the next read resolves the file again.
1059
+
1060
+ The registries and stores the Config holds go with it, and a store singleton resolves
1061
+ its backend per call, so it follows. Only an explicitly installed object persists.
1062
+ """
1063
+ global _config
1064
+ _config = None