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,609 @@
1
+ """Namespace-addressed stores: an ``@local/…`` id lives in the ``@local`` namespace's local stores,
2
+ every other id in the configured ones.
3
+
4
+ Routing runs only once a process turns it on (the CLI does, for the length of a command). Everywhere
5
+ else each getter returns the configured store exactly as before, so a service keeps rejecting every
6
+ ``@`` id. The ``@local`` namespace keeps its documents in a file of its own beside the default local
7
+ store's, so each file only ever holds one namespace's documents; objects and images share the
8
+ per-user object store and local registry, whose urls and repositories say whose they are.
9
+
10
+ Entity documents route by their id. What a task run records (instances, journals, state, runs,
11
+ reviews, conversations, snapshots) and the objects it writes under keys of its own follow the run
12
+ scope ``Task.run()`` sets from its task id. A read that neither rule places reads both stores, the
13
+ reader's own namespace first: an ``@local`` run sees its records before the registry's, and anyone
14
+ else sees the registry's first. While an ``@local`` task runs, a write that would reach a configured
15
+ store is refused (the write fence), and a bare entity may never name an ``@local`` id (references
16
+ point outward only).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import re
23
+ import sqlite3
24
+ import threading
25
+ from collections.abc import Iterator, Mapping
26
+ from contextlib import contextmanager
27
+ from contextvars import ContextVar
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+ from typing import Any, BinaryIO, Callable, Optional, TypeVar
31
+
32
+ from agent_env.config.errors import ConfigError
33
+ from agent_env.store.document_store.document_store import (
34
+ DocumentStore,
35
+ Eq,
36
+ Filter,
37
+ In,
38
+ Sort,
39
+ UpdateSpec,
40
+ _dig,
41
+ is_entity_collection,
42
+ )
43
+ from agent_env.store.document_store.sqlite_document_store import LocalSqliteDocumentStore
44
+ from agent_env.store.ids import LOCAL_NAMESPACE, LOCAL_PREFIX, is_local_id, parse_namespace, validate_local_id
45
+ from agent_env.store.image_store import ImageStore, OciRegistryImageStore
46
+ from agent_env.store.image_store.oci_registry_credentials import (
47
+ RegistryAuth,
48
+ normalize_registry_host,
49
+ registry_host_from_ref,
50
+ )
51
+ from agent_env.store.object_store import ObjectStore
52
+ from agent_env.store.object_store.object_store import (
53
+ DEFAULT_CONTENT_TYPE,
54
+ HttpGetGrant,
55
+ HttpPutGrant,
56
+ ObjectMetadata,
57
+ UploadPolicy,
58
+ )
59
+
60
+ # The fields a run-time document names its owner by: one holding an @local id places it locally.
61
+ _OWNER_FIELDS = ("id", "instance_id", "task_id", "task_instance_id", "env_id", "agent_id")
62
+ _LOCAL_REPOSITORY_PREFIX = f"{LOCAL_NAMESPACE}/"
63
+ _TIMESTAMP = re.compile(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}")
64
+ _T = TypeVar("_T")
65
+
66
+ _enabled = False
67
+ _scopes = 0
68
+ _scopes_lock = threading.Lock()
69
+ _run_namespace: ContextVar[Optional[str]] = ContextVar("agent_env_run_namespace", default=None)
70
+
71
+
72
+ class LocalRunWriteError(ValueError):
73
+ """A write an ``@local`` task run may not make, since it would land in a configured store."""
74
+
75
+
76
+ class OutwardReferenceError(ValueError):
77
+ """A write that would point outside the ``@local`` namespace's stores at an ``@local`` id."""
78
+
79
+
80
+ def enable_namespace_routing() -> None:
81
+ """Route ``@local`` ids to the per-user local stores in this process. The CLI turns this on; a
82
+ service never does, so its stores stay exactly the configured ones."""
83
+ global _enabled
84
+ _enabled = True
85
+
86
+
87
+ def disable_namespace_routing() -> None:
88
+ global _enabled
89
+ _enabled = False
90
+
91
+
92
+ def namespace_routing_enabled() -> bool:
93
+ return _enabled or _scopes > 0
94
+
95
+
96
+ @contextmanager
97
+ def namespace_routing() -> Iterator[None]:
98
+ """Route namespaces for the length of the block. Overlapping blocks (two commands run in one
99
+ process) keep routing on until the last of them ends."""
100
+ global _scopes
101
+ with _scopes_lock:
102
+ _scopes += 1
103
+ try:
104
+ yield
105
+ finally:
106
+ with _scopes_lock:
107
+ _scopes -= 1
108
+
109
+
110
+ @contextmanager
111
+ def run_scope(owner_id: str) -> Iterator[None]:
112
+ """The run in progress, a task's or a deployment the CLI makes: what it records follows its owner id's
113
+ namespace. A run started inside an ``@local`` run stays local, so a nested registry task can't reopen
114
+ the configured stores."""
115
+ outer = _run_namespace.get()
116
+ token = _run_namespace.set(LOCAL_NAMESPACE if outer == LOCAL_NAMESPACE else parse_namespace(owner_id) or "")
117
+ try:
118
+ yield
119
+ finally:
120
+ _run_namespace.reset(token)
121
+
122
+
123
+ def in_local_run() -> bool:
124
+ return namespace_routing_enabled() and _run_namespace.get() == LOCAL_NAMESPACE
125
+
126
+
127
+ def refuse_in_local_run(what: str) -> None:
128
+ if in_local_run():
129
+ raise LocalRunWriteError(
130
+ f"an @local task run writes only to the local stores, and {what} would reach a configured store"
131
+ )
132
+
133
+
134
+ def refuse_local_derivation(entity_id: str, kind: str, doing: str) -> None:
135
+ """Refuse ``doing`` to an ``@local`` entity whose tasks, artifacts or objects are named after it
136
+ in a form that doesn't keep the ``@local`` namespace yet."""
137
+ if is_local_id(entity_id):
138
+ raise ValueError(f"{entity_id!r} is an @local {kind}, and {doing} one isn't supported yet")
139
+
140
+
141
+ def refuse_local_references(entity_id: object, value: Any) -> None:
142
+ """Refuse a bare entity naming an ``@local`` id anywhere in ``value``: a shared store must
143
+ never point into one person's local stores."""
144
+ path = _local_reference(value, "")
145
+ if path is not None:
146
+ raise OutwardReferenceError(
147
+ f"{entity_id!r} is not an @local id, so it can't reference one: {path or 'its value'} names an @local id"
148
+ )
149
+
150
+
151
+ def _local_reference(value: Any, path: str) -> Optional[str]:
152
+ if isinstance(value, str):
153
+ return path if is_local_id(value) else None
154
+ if isinstance(value, Mapping):
155
+ items = ((f"{path}.{key}" if path else str(key), item) for key, item in value.items())
156
+ elif isinstance(value, (list, tuple, set)):
157
+ items = ((f"{path}[{index}]", item) for index, item in enumerate(value))
158
+ else:
159
+ return None
160
+ return next((found for item_path, item in items if (found := _local_reference(item, item_path)) is not None), None)
161
+
162
+
163
+ def _update_values(update: UpdateSpec) -> dict:
164
+ return {"set": update.set, "add_to_set": update.add_to_set, "push": update.push}
165
+
166
+
167
+ def _merge_sort(docs: list[dict], sort: Optional[Sort], *, absent_last: bool = False) -> list[dict]:
168
+ """Order documents from both stores the way each store orders its own: an absent or null value
169
+ first ascending and last descending (``query``, ``find_one``), or last either way with
170
+ ``absent_last`` (``latest_per_id``); a datetime (Mongo's) and an ISO timestamp string (SQLite's)
171
+ compare as times."""
172
+ if not sort or not sort.keys:
173
+ return docs
174
+ ordered = list(docs)
175
+ for key in reversed(sort.keys):
176
+ values = {id(doc): _dig(doc, key.field) for doc in ordered}
177
+ present = [doc for doc in ordered if values[id(doc)][0] and values[id(doc)][1] is not None]
178
+ absent = [doc for doc in ordered if not (values[id(doc)][0] and values[id(doc)][1] is not None)]
179
+ present.sort(key=lambda doc: _comparable(values[id(doc)][1]), reverse=key.descending)
180
+ ordered = present + absent if absent_last or key.descending else absent + present
181
+ return ordered
182
+
183
+
184
+ def _comparable(value: Any) -> tuple:
185
+ if isinstance(value, str) and _TIMESTAMP.match(value):
186
+ try:
187
+ value = datetime.fromisoformat(value)
188
+ except ValueError:
189
+ return ("str", value)
190
+ if isinstance(value, datetime):
191
+ return ("time", value if value.tzinfo else value.replace(tzinfo=timezone.utc))
192
+ if isinstance(value, bool):
193
+ return ("bool", value)
194
+ if isinstance(value, (int, float)):
195
+ return ("number", value)
196
+ if isinstance(value, (dict, list)):
197
+ return ("json", json.dumps(value, sort_keys=True, default=str))
198
+ return (type(value).__name__, value)
199
+
200
+
201
+ def _ids_split_by_namespace(collection: str, id_field: str) -> bool:
202
+ """Whether no id can be in both stores: an entity's id decides which one it is written to."""
203
+ return is_entity_collection(collection) and id_field == "id"
204
+
205
+
206
+ def _newer(version: Any, than: Any) -> bool:
207
+ """Whether ``version`` is a number above ``than``; anything but a number is older than any number."""
208
+ def number(value: Any) -> bool:
209
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
210
+
211
+ return number(version) and (not number(than) or version > than)
212
+
213
+
214
+ def _window(docs: list[dict], limit: Optional[int], offset: Optional[int]) -> list[dict]:
215
+ docs = docs[offset or 0:]
216
+ return docs[:limit] if limit else docs
217
+
218
+
219
+ def _filter_owner(filter: Filter) -> dict:
220
+ """The field values an all-equality filter pins, the document an upsert would create."""
221
+ return {path: preds[0].value for path, preds in filter.conditions.items() if len(preds) == 1 and isinstance(preds[0], Eq)}
222
+
223
+
224
+ def _filter_is_local(filter: Filter) -> Optional[bool]:
225
+ """Whether the ids a filter pins are all ``@local`` (True) or all bare (False); None when it pins
226
+ none, or both kinds."""
227
+ values: list[Any] = []
228
+ for pred in filter.conditions.get("id", ()):
229
+ if isinstance(pred, Eq):
230
+ values.append(pred.value)
231
+ elif isinstance(pred, In):
232
+ values.extend(pred.values)
233
+ kinds = {is_local_id(value) for value in values if isinstance(value, str)}
234
+ return kinds.pop() if len(kinds) == 1 else None
235
+
236
+
237
+ class LocalNamespaceDocumentStore(LocalSqliteDocumentStore):
238
+ """The ``@local`` namespace's documents: its entities, and what its runs record."""
239
+
240
+ def check_id(self, entity_id: str) -> None:
241
+ if not is_local_id(entity_id):
242
+ raise ValueError(f"id {entity_id!r} isn't an @local id, and the @local namespace's store holds only those")
243
+ validate_local_id(entity_id)
244
+
245
+
246
+ class RoutingDocumentStore(DocumentStore):
247
+ """The configured document store, with ``@local`` documents and ``@local`` runs' records kept in
248
+ the ``@local`` namespace's store. Reads never create that store; writes open it with its indexes."""
249
+
250
+ def __init__(self, configured: DocumentStore, local: DocumentStore, local_path: Path) -> None:
251
+ self.configured = configured
252
+ self.local = local
253
+ self._local_path = local_path
254
+ self._lock = threading.Lock()
255
+ self._pending_local_indexes: Optional[list[tuple]] = []
256
+
257
+ def find_one(self, collection: str, filter: Filter, sort: Optional[Sort] = None) -> Optional[dict]:
258
+ found = [
259
+ doc for store in self._readers(collection, filter)
260
+ if (doc := self._read(store, lambda: store.find_one(collection, filter, sort))) is not None
261
+ ]
262
+ return _merge_sort(found, sort)[0] if found else None
263
+
264
+ def query(
265
+ self,
266
+ collection: str,
267
+ filter: Filter,
268
+ sort: Optional[Sort] = None,
269
+ limit: Optional[int] = None,
270
+ offset: Optional[int] = None,
271
+ ) -> list[dict]:
272
+ readers = self._readers(collection, filter)
273
+ if len(readers) == 1:
274
+ return self._read(readers[0], lambda: readers[0].query(collection, filter, sort, limit, offset))
275
+ window = (offset or 0) + limit if limit else None
276
+ merged = [doc for store in readers for doc in self._read(store, lambda: store.query(collection, filter, sort, window))]
277
+ return _window(_merge_sort(merged, sort), limit, offset)
278
+
279
+ def count(self, collection: str, filter: Filter) -> int:
280
+ return sum(self._read(store, lambda: store.count(collection, filter)) for store in self._readers(collection, filter))
281
+
282
+ def latest_per_id(
283
+ self,
284
+ collection: str,
285
+ filter: Filter,
286
+ *,
287
+ id_field: str = "id",
288
+ version_field: str = "version",
289
+ sort: Optional[Sort] = None,
290
+ limit: Optional[int] = None,
291
+ offset: Optional[int] = None,
292
+ ) -> list[dict]:
293
+ def latest(store: DocumentStore, **page: Any) -> list[dict]:
294
+ return store.latest_per_id(collection, filter, id_field=id_field, version_field=version_field, sort=sort, **page)
295
+
296
+ readers = self._readers(collection, filter)
297
+ if len(readers) == 1:
298
+ return self._read(readers[0], lambda: latest(readers[0], limit=limit, offset=offset))
299
+ if _ids_split_by_namespace(collection, id_field):
300
+ window = (offset or 0) + limit if limit else None
301
+ merged = [doc for store in readers for doc in self._read(store, lambda: latest(store, limit=window))]
302
+ else:
303
+ merged = self._latest_across(readers, collection, filter, id_field, version_field)
304
+ return _window(_merge_sort(merged, sort, absent_last=True), limit, offset)
305
+
306
+ def count_distinct(self, collection: str, filter: Filter, *, id_field: str = "id") -> int:
307
+ readers = self._readers(collection, filter)
308
+ if len(readers) == 1 or _ids_split_by_namespace(collection, id_field):
309
+ return sum(
310
+ self._read(store, lambda: store.count_distinct(collection, filter, id_field=id_field)) for store in readers
311
+ )
312
+ return len(self._latest_across(readers, collection, filter, id_field, "version"))
313
+
314
+ def _latest_across(
315
+ self, readers: list[DocumentStore], collection: str, filter: Filter, id_field: str, version_field: str,
316
+ ) -> list[dict]:
317
+ """Each id's latest document across both stores, where one id can be in both: the higher
318
+ version wins, and a tie goes to the reader's own namespace."""
319
+ latest: dict[str, dict] = {}
320
+ for store in readers:
321
+ docs = self._read(
322
+ store, lambda: store.latest_per_id(collection, filter, id_field=id_field, version_field=version_field)
323
+ )
324
+ for doc in docs:
325
+ key = json.dumps(_dig(doc, id_field)[1], sort_keys=True, default=str)
326
+ held = latest.get(key)
327
+ if held is None or _newer(_dig(doc, version_field)[1], _dig(held, version_field)[1]):
328
+ latest[key] = doc
329
+ return list(latest.values())
330
+
331
+ def insert(self, collection: str, doc: dict) -> None:
332
+ store = self._writer(collection, doc, doc)
333
+ self._read(store, lambda: store.insert(collection, doc))
334
+
335
+ def update(self, collection: str, filter: Filter, update: UpdateSpec, upsert: bool = False) -> int:
336
+ if upsert:
337
+ store = self._writer(collection, _filter_owner(filter), _update_values(update))
338
+ return self._read(store, lambda: store.update(collection, filter, update, upsert=True))
339
+ for store in self._updaters(collection, filter, _update_values(update)):
340
+ if matched := self._read(store, lambda: store.update(collection, filter, update)):
341
+ return matched
342
+ return 0
343
+
344
+ def update_one_and_get(
345
+ self,
346
+ collection: str,
347
+ filter: Filter,
348
+ update: UpdateSpec,
349
+ return_after: bool = True,
350
+ upsert: bool = False,
351
+ ) -> Optional[dict]:
352
+ if upsert:
353
+ store = self._writer(collection, _filter_owner(filter), _update_values(update))
354
+ return self._read(store, lambda: store.update_one_and_get(collection, filter, update, return_after, upsert=True))
355
+ for store in self._updaters(collection, filter, _update_values(update)):
356
+ if (doc := self._read(store, lambda: store.update_one_and_get(collection, filter, update, return_after))) is not None:
357
+ return doc
358
+ return None
359
+
360
+ def replace(self, collection: str, filter: Filter, doc: dict, upsert: bool = False) -> int:
361
+ if upsert:
362
+ store = self._writer(collection, {**_filter_owner(filter), **doc}, doc)
363
+ return self._read(store, lambda: store.replace(collection, filter, doc, upsert=True))
364
+ for store in self._updaters(collection, filter, doc):
365
+ if matched := self._read(store, lambda: store.replace(collection, filter, doc)):
366
+ return matched
367
+ return 0
368
+
369
+ def delete(self, collection: str, filter: Filter) -> int:
370
+ for store in self._updaters(collection, filter, None):
371
+ if deleted := self._read(store, lambda: store.delete(collection, filter)):
372
+ return deleted
373
+ return 0
374
+
375
+ def ensure_index(
376
+ self,
377
+ collection: str,
378
+ fields: list[str],
379
+ unique: bool = False,
380
+ ttl_seconds: Optional[int] = None,
381
+ ) -> None:
382
+ self.configured.ensure_index(collection, fields, unique, ttl_seconds)
383
+ with self._lock:
384
+ if self._pending_local_indexes is not None:
385
+ self._pending_local_indexes.append((collection, fields, unique, ttl_seconds))
386
+ return
387
+ self.local.ensure_index(collection, fields, unique, ttl_seconds)
388
+
389
+ def check_id(self, entity_id: str) -> None:
390
+ if is_local_id(entity_id):
391
+ self.local.check_id(entity_id)
392
+ return
393
+ if LOCAL_PREFIX in entity_id:
394
+ raise ValueError(
395
+ f"id {entity_id!r} contains an @local id but isn't one: an id derived from an @local id "
396
+ "has to keep the @local namespace"
397
+ )
398
+ self.configured.check_id(entity_id)
399
+
400
+ def _placed(self, collection: str, filter: Filter) -> Optional[DocumentStore]:
401
+ if not is_entity_collection(collection):
402
+ return None
403
+ local = _filter_is_local(filter)
404
+ return None if local is None else self.local if local else self.configured
405
+
406
+ def _readers(self, collection: str, filter: Filter) -> list[DocumentStore]:
407
+ """The stores a read consults, the reader's own namespace first: a key can be in both, when
408
+ an ``@local`` run and a registry run each recorded it."""
409
+ placed = self._placed(collection, filter)
410
+ if placed is self.configured:
411
+ return [self.configured]
412
+ local = [self.local] if self._local_path.exists() else []
413
+ if placed is self.local:
414
+ return local
415
+ return [*local, self.configured] if in_local_run() else [self.configured, *local]
416
+
417
+ def _read(self, store: DocumentStore, read: Callable[[], _T]) -> _T:
418
+ if store is not self.local:
419
+ return read()
420
+ try:
421
+ return read()
422
+ except (sqlite3.DatabaseError, OSError) as e:
423
+ raise ConfigError(
424
+ f"the @local store at {self._local_path} can't be read ({e}); move it aside to start a new one"
425
+ ) from e
426
+
427
+ def _writer(self, collection: str, owner: Mapping, payload: Any) -> DocumentStore:
428
+ if is_entity_collection(collection):
429
+ if isinstance(owner.get("id"), str):
430
+ self.check_id(owner["id"])
431
+ local = isinstance(owner.get("id"), str) and is_local_id(owner["id"])
432
+ else:
433
+ local = in_local_run() or any(isinstance(owner.get(f), str) and is_local_id(owner[f]) for f in _OWNER_FIELDS)
434
+ if local:
435
+ return self._open_local()
436
+ self._check_configured_write(collection, owner.get("id"), payload)
437
+ return self.configured
438
+
439
+ def _updaters(self, collection: str, filter: Filter, payload: Any) -> Iterator[DocumentStore]:
440
+ """The stores an update of ``filter``'s match tries, in order: the one its id places it in,
441
+ else the caller's own namespace's first. Inside an ``@local`` run only the local store is
442
+ written, and a match that lives only in the configured store is refused rather than missed."""
443
+ placed = self._placed(collection, filter)
444
+ local = placed is not self.configured and self._local_path.exists()
445
+ if placed is None and not in_local_run():
446
+ self._check_configured_write(collection, _filter_owner(filter).get("id"), payload)
447
+ yield self.configured
448
+ if local:
449
+ yield self._open_local()
450
+ return
451
+ if local:
452
+ yield self._open_local()
453
+ if placed is self.configured:
454
+ self._check_configured_write(collection, _filter_owner(filter).get("id"), payload)
455
+ yield self.configured
456
+ elif placed is None and self.configured.find_one(collection, filter) is not None:
457
+ refuse_in_local_run(f"updating {collection!r} (its record is in the configured store)")
458
+
459
+ def _check_configured_write(self, collection: str, entity_id: object, payload: Any) -> None:
460
+ refuse_in_local_run(f"this write to {collection!r}")
461
+ if is_entity_collection(collection) and payload is not None:
462
+ refuse_local_references(entity_id, payload)
463
+
464
+ def _open_local(self) -> DocumentStore:
465
+ with self._lock:
466
+ for spec in self._pending_local_indexes or ():
467
+ self._read(self.local, lambda: self.local.ensure_index(*spec))
468
+ self._pending_local_indexes = None
469
+ return self.local
470
+
471
+
472
+ def object_store_holding(object_url: str, configured: ObjectStore, local: ObjectStore) -> ObjectStore:
473
+ """The store an object_url addresses: the local store's own urls are local unless the configured
474
+ store owns them too, and any other url is the configured store's, as it was before routing."""
475
+ return local if local.owns(object_url) and not configured.owns(object_url) else configured
476
+
477
+
478
+ def configured_store(store: Any) -> Any:
479
+ """The configured store behind ``store``: a routing store's or view's configured side, else
480
+ ``store`` itself. For reports that name the backend a user configured."""
481
+ return store.configured if isinstance(store, (RoutingDocumentStore, LocalRunObjectStore, LocalRunImageStore)) else store
482
+
483
+
484
+ def image_store_holding(ref: str, configured: ImageStore, local: ImageStore) -> ImageStore:
485
+ host = registry_host_from_ref(ref)
486
+ if isinstance(local, OciRegistryImageStore) and host == normalize_registry_host(local.registry_host):
487
+ return local
488
+ return configured
489
+
490
+
491
+ class LocalRunObjectStore(ObjectStore):
492
+ """The object store an ``@local`` task run sees: keys go to the local store, an object_url to the
493
+ store it addresses, and a write that would reach the configured store is refused."""
494
+
495
+ def __init__(self, configured: ObjectStore, local: ObjectStore) -> None:
496
+ self.configured = configured
497
+ self.local = local
498
+
499
+ @property
500
+ def supports_transfer_grants(self) -> bool:
501
+ return self.local.supports_transfer_grants
502
+
503
+ @property
504
+ def max_single_upload_bytes(self) -> int | None:
505
+ return self.local.max_single_upload_bytes
506
+
507
+ def put(self, key: str, data: bytes, content_type: str = DEFAULT_CONTENT_TYPE, allow_overwrite: bool = False) -> str:
508
+ return self.local.put(key, data, content_type, allow_overwrite)
509
+
510
+ def put_file(self, key: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
511
+ return self.local.put_file(key, file_path, content_type)
512
+
513
+ def put_file_at(self, object_url: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
514
+ return self._writing(object_url).put_file_at(object_url, file_path, content_type)
515
+
516
+ def get(self, object_url: str) -> bytes:
517
+ return self._at(object_url).get(object_url)
518
+
519
+ def exists(self, key: str) -> bool:
520
+ return self.local.exists(key)
521
+
522
+ def get_object_metadata(self, key: str) -> ObjectMetadata | None:
523
+ return self.local.get_object_metadata(key)
524
+
525
+ def get_object_metadata_at(self, object_url: str) -> ObjectMetadata | None:
526
+ return self._at(object_url).get_object_metadata_at(object_url)
527
+
528
+ def list(self, prefix: str) -> list[str]:
529
+ return self.local.list(prefix)
530
+
531
+ def list_at(self, url_prefix: str) -> list[str]:
532
+ return self._at(url_prefix).list_at(url_prefix)
533
+
534
+ def object_url(self, key: str) -> str:
535
+ return self.local.object_url(key)
536
+
537
+ def get_object_key(self, object_url: str) -> str:
538
+ return self._at(object_url).get_object_key(object_url)
539
+
540
+ def owns(self, object_url: str) -> bool:
541
+ return self.local.owns(object_url) or self.configured.owns(object_url)
542
+
543
+ def download_to_file(self, object_url: str, dest_path: str) -> None:
544
+ self._at(object_url).download_to_file(object_url, dest_path)
545
+
546
+ def open(self, object_url: str) -> BinaryIO:
547
+ return self._at(object_url).open(object_url)
548
+
549
+ def read(self, key: str) -> bytes:
550
+ return self.local.read(key)
551
+
552
+ def signed_get_url(self, object_url: str, expires_in: int = 3600) -> str | None:
553
+ return self._at(object_url).signed_get_url(object_url, expires_in)
554
+
555
+ def signed_put_url(self, object_url: str, expires_in: int = 3600) -> str | None:
556
+ return self._writing(object_url).signed_put_url(object_url, expires_in)
557
+
558
+ def signed_post(self, url_prefix: str, *, expires_in: int = 3600, max_bytes: int | None = None) -> dict | None:
559
+ return self._writing(url_prefix).signed_post(url_prefix, expires_in=expires_in, max_bytes=max_bytes)
560
+
561
+ def shared_credentials_env(self) -> dict[str, str]:
562
+ return self.configured.shared_credentials_env()
563
+
564
+ def issue_read_grant(self, object_url: str, *, expires_in: int = 3600) -> HttpGetGrant:
565
+ return self._at(object_url).issue_read_grant(object_url, expires_in=expires_in)
566
+
567
+ def issue_write_grant(self, object_url: str, *, media_type: str, max_bytes: int, expires_in: int = 3600) -> HttpPutGrant:
568
+ return self._writing(object_url).issue_write_grant(
569
+ object_url, media_type=media_type, max_bytes=max_bytes, expires_in=expires_in
570
+ )
571
+
572
+ def issue_upload_policy(self, prefix_url: str, *, max_object_bytes: int, expires_in: int) -> UploadPolicy:
573
+ return self._writing(prefix_url).issue_upload_policy(prefix_url, max_object_bytes=max_object_bytes, expires_in=expires_in)
574
+
575
+ def _at(self, object_url: str) -> ObjectStore:
576
+ return object_store_holding(object_url, self.configured, self.local)
577
+
578
+ def _writing(self, object_url: str) -> ObjectStore:
579
+ store = self._at(object_url)
580
+ if store is self.configured:
581
+ refuse_in_local_run(f"a write to {object_url!r}")
582
+ return store
583
+
584
+
585
+ class LocalRunImageStore(ImageStore):
586
+ """The image store an ``@local`` task run sees: ``@local`` repositories are the local registry's,
587
+ pull auth comes from the store a ref names, and creating a configured repository is refused."""
588
+
589
+ def __init__(self, configured: ImageStore, local: ImageStore) -> None:
590
+ self.configured = configured
591
+ self.local = local
592
+
593
+ def image_ref(self, repository: str, tag: str) -> str:
594
+ return self._for(repository).image_ref(repository, tag)
595
+
596
+ def ensure_repository(self, repository: str) -> None:
597
+ store = self._for(repository)
598
+ if store is self.configured:
599
+ refuse_in_local_run(f"creating repository {repository!r}")
600
+ store.ensure_repository(repository)
601
+
602
+ def owns(self, ref: str) -> bool:
603
+ return image_store_holding(ref, self.configured, self.local).owns(ref)
604
+
605
+ def auth(self, ref: str) -> RegistryAuth | None:
606
+ return image_store_holding(ref, self.configured, self.local).auth(ref)
607
+
608
+ def _for(self, repository: str) -> ImageStore:
609
+ return self.local if repository.startswith(_LOCAL_REPOSITORY_PREFIX) else self.configured
@@ -0,0 +1,19 @@
1
+ """Backend-agnostic secret store abstraction + its implementations.
2
+
3
+ ``secret_store`` defines the abstraction (SecretStore); ``aws_secrets_manager_secret_store``
4
+ and ``local_secret_store`` are implementations. ``gcp_secret_manager_secret_store`` is one
5
+ too, not re-exported here because it needs the ``gcp`` extra. Future backends (Vault, Azure
6
+ Key Vault, K8s secrets, ...) live alongside as new modules.
7
+ """
8
+
9
+ from agent_env.store.secret_store.aws_secrets_manager_secret_store import (
10
+ AwsSecretsManagerSecretStore,
11
+ )
12
+ from agent_env.store.secret_store.local_secret_store import LocalSecretStore
13
+ from agent_env.store.secret_store.secret_store import SecretStore
14
+
15
+ __all__ = [
16
+ "SecretStore",
17
+ "AwsSecretsManagerSecretStore",
18
+ "LocalSecretStore",
19
+ ]