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,295 @@
1
+ """Artifact store for versioned documents and binary objects via the pluggable stores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import threading
7
+ import uuid
8
+ from datetime import datetime, timezone
9
+ from typing import TYPE_CHECKING, Optional, Self, TypeVar
10
+
11
+ from agent_env.config import get_config
12
+ from agent_env.plugins import _registration
13
+
14
+ logger = logging.getLogger(__name__)
15
+ from agent_env.artifact.registry import canonical_type, equivalent_types, get_artifact_registry
16
+ from agent_env.store.base import NotFoundError
17
+ from agent_env.store.document_store import (
18
+ DuplicateKeyError,
19
+ Filter,
20
+ In,
21
+ UpdateSpec,
22
+ VersionedEntityStore,
23
+ VersionedEntityStoreCache,
24
+ )
25
+ from agent_env.store.ids import key_segment
26
+ from agent_env.store.query import QueryBuilder, to_document_query
27
+
28
+ if TYPE_CHECKING:
29
+ from agent_env.artifact.artifact import Artifact
30
+
31
+ _A = TypeVar("_A", bound="Artifact")
32
+
33
+ ARTIFACTS_COLLECTION = "artifacts"
34
+
35
+ # Retry cap for the version-allocation race in put_document.
36
+ _MAX_VERSION_RETRIES = 3
37
+
38
+ _write_locks: dict[str, threading.Lock] = {}
39
+ _write_locks_guard = threading.Lock()
40
+
41
+
42
+ def artifact_write_lock(artifact_id: str) -> threading.Lock:
43
+ """A lock for writing ``artifact_id`` from this process. A write that allocates a version, puts
44
+ objects at paths named by it and then records the document takes the same version as any
45
+ other write of that id running alongside it; hold this around the whole write to take turns."""
46
+ with _write_locks_guard:
47
+ return _write_locks.setdefault(artifact_id, threading.Lock())
48
+
49
+ # Fields that address the document rather than describe it, so `set_field` must not touch them.
50
+ _IDENTITY_FIELDS = frozenset({"id", "version", "type"})
51
+
52
+
53
+ # Hex digits of the attempt part of an object prefix: enough that two attempts never meet.
54
+ _ATTEMPT_ID_LENGTH = 8
55
+
56
+
57
+ class ArtifactQuery(QueryBuilder["Artifact"]):
58
+ """Chainable query builder for artifacts.
59
+
60
+ Example:
61
+ artifact = Artifact.query().id("my_artifact").latest().first()
62
+ artifacts = Artifact.query().type("docker_image").execute()
63
+ artifacts = Artifact.query().version_gte(2).execute()
64
+ """
65
+
66
+ def __init__(self, store: Optional["ArtifactStore"] = None) -> None:
67
+ super().__init__()
68
+ self._store = store
69
+
70
+ def _clone(self) -> Self:
71
+ clone = ArtifactQuery(self._store)
72
+ clone._filters = self._filters.copy()
73
+ clone._sort_field = self._sort_field
74
+ clone._sort_desc = self._sort_desc
75
+ clone._limit_value = self._limit_value
76
+ clone._offset_value = self._offset_value
77
+ return clone
78
+
79
+ def type(self, artifact_type: str) -> Self:
80
+ spellings = equivalent_types(artifact_type)
81
+ if len(spellings) == 1:
82
+ return self._add_filter("type", artifact_type)
83
+ return self._add_filter("type_in", spellings)
84
+
85
+ def execute(self) -> list["Artifact"]:
86
+ if self._store is None:
87
+ self._store = get_artifact_store()
88
+ return self._store.execute_query(self)
89
+
90
+ def _execute_count(self) -> int:
91
+ if self._store is None:
92
+ self._store = get_artifact_store()
93
+ return self._store.execute_count(self)
94
+
95
+
96
+ class ArtifactStore:
97
+ """Store for artifact persistence in MongoDB and S3."""
98
+
99
+ def __init__(self) -> None:
100
+ self._versioned_cache: VersionedEntityStoreCache["Artifact"] = VersionedEntityStoreCache(
101
+ ARTIFACTS_COLLECTION, None, self._deserialize, secondary_indexes=[["type"]],
102
+ )
103
+
104
+ @property
105
+ def _doc_store(self):
106
+ return get_config().get_document_store()
107
+
108
+ @property
109
+ def _versioned(self) -> VersionedEntityStore["Artifact"]:
110
+ return self._versioned_cache.for_store(self._doc_store)
111
+
112
+ def get(self, id: str, version: Optional[int] = None) -> "Artifact":
113
+ """Get an artifact by id. Returns latest version if version not specified."""
114
+ artifact = self._versioned.get(id, version)
115
+ if artifact is None:
116
+ version_str = f" version={version}" if version is not None else ""
117
+ raise NotFoundError(f"Artifact {id}{version_str} not found")
118
+ return artifact
119
+
120
+ def next_version(self, id: str) -> int:
121
+ # Every put asks for its version before writing an object or pushing an image, so an id
122
+ # the store refuses, or a bare one while an @local task runs, is refused here rather than
123
+ # after those side effects, in put_document.
124
+ self._doc_store.check_id(id)
125
+ get_config().check_local_run_write(id)
126
+ return self._versioned.next_version(id)
127
+
128
+ def attempt_prefix(self, artifact_type: str, id: str) -> str:
129
+ """An object-store prefix of its own for one write of ``id``'s next version. Objects are written
130
+ once, so a write that fails partway must not leave its objects where the next attempt writes."""
131
+ attempt = f"{self.next_version(id)}-{uuid.uuid4().hex[:_ATTEMPT_ID_LENGTH]}"
132
+ key = f"{get_config().get_artifact_key_prefix()}artifacts/{artifact_type}/{key_segment(id)}/{attempt}"
133
+ return get_config().get_object_store_for(id).object_url(key).rstrip("/") + "/"
134
+
135
+ def put_object(
136
+ self,
137
+ artifact_type: str,
138
+ id: str,
139
+ version: int,
140
+ object_name: str,
141
+ data: bytes,
142
+ content_type: str = "application/json",
143
+ allow_overwrite: bool = False,
144
+ ) -> str:
145
+ """Upload an artifact object; return its object URL.
146
+
147
+ Write-once by default (raises ObjectAlreadyExistsError on collision);
148
+ set allow_overwrite=True for mutable data (e.g. rolling run snapshots).
149
+ """
150
+ key = self._object_key(artifact_type, id, version, object_name)
151
+ return get_config().get_object_store_for(id).put(key, data, content_type, allow_overwrite)
152
+
153
+ def put_object_file(
154
+ self,
155
+ artifact_type: str,
156
+ id: str,
157
+ version: int,
158
+ object_name: str,
159
+ file_path: str,
160
+ content_type: str = "application/octet-stream",
161
+ ) -> str:
162
+ """Write-once upload of a local file (multipart for large files); return its object URL."""
163
+ key = self._object_key(artifact_type, id, version, object_name)
164
+ return get_config().get_object_store_for(id).put_file(key, file_path, content_type)
165
+
166
+ @staticmethod
167
+ def _object_key(artifact_type: str, id: str, version: int, object_name: str) -> str:
168
+ return f"{get_config().get_artifact_key_prefix()}artifacts/{artifact_type}/{key_segment(id)}/{version}/{object_name}"
169
+
170
+ def get_object(self, object_url: str) -> bytes:
171
+ """Download an artifact object by its object_url."""
172
+ return get_config().get_object_store_at(object_url).get(object_url)
173
+
174
+ def put_document(self, artifact: _A) -> _A:
175
+ """Insert an artifact document, retrying the version-allocation race.
176
+
177
+ Concurrent writers to one id can allocate the same next_version; the
178
+ unique index rejects the losers, so we re-allocate and retry rather
179
+ than fail the caller (which drops a pass@k rollout).
180
+ """
181
+
182
+ self._doc_store.check_id(artifact.id)
183
+ if artifact.version < 0:
184
+ raise ValueError(f"Artifact version must be non-negative, got {artifact.version}")
185
+
186
+ store = self._doc_store
187
+ versioned = self._versioned_cache.for_store(store)
188
+ version = artifact.version if artifact.version > 0 else versioned.next_version(artifact.id)
189
+ doc = artifact.model_dump(by_alias=True)
190
+ doc["created_at_utc"] = datetime.now(timezone.utc)
191
+ last_exc: Optional[DuplicateKeyError] = None
192
+ for attempt in range(_MAX_VERSION_RETRIES):
193
+ doc["version"] = version
194
+ try:
195
+ store.insert(ARTIFACTS_COLLECTION, dict(doc))
196
+ return artifact.model_copy(update={"version": version})
197
+ except DuplicateKeyError as e:
198
+ last_exc = e
199
+ if attempt == _MAX_VERSION_RETRIES - 1:
200
+ break
201
+ version = versioned.next_version(artifact.id)
202
+ logger.warning(
203
+ "artifact %s version taken (concurrent write); retrying at v%s (%s/%s)",
204
+ artifact.id, version, attempt + 2, _MAX_VERSION_RETRIES,
205
+ )
206
+ assert last_exc is not None
207
+ raise last_exc
208
+
209
+ def latest_by_type(
210
+ self, artifact_type: str, ids: Optional[list[str]] = None, limit: int = 50
211
+ ) -> list["Artifact"]:
212
+ """Return the latest version of each artifact of a type, newest version first.
213
+
214
+ Optionally restricted to ``ids``. Groups all matching versions in Python
215
+ (the DocumentStore has no aggregation), so this is a browse/CLI helper,
216
+ not a hot path.
217
+ """
218
+ spellings = equivalent_types(artifact_type)
219
+ filt = (
220
+ Filter.of(type=artifact_type) if len(spellings) == 1
221
+ else Filter({}).where("type", In(spellings))
222
+ )
223
+ if ids is not None:
224
+ filt = filt.where("id", In(ids))
225
+ latest: dict[str, dict] = {}
226
+ for doc in self._doc_store.query(ARTIFACTS_COLLECTION, filt):
227
+ current = latest.get(doc["id"])
228
+ if current is None or doc["version"] > current["version"]:
229
+ latest[doc["id"]] = doc
230
+ ordered = sorted(latest.values(), key=lambda d: (-d["version"], d["id"]))[:limit]
231
+ return [self._deserialize(d) for d in ordered]
232
+
233
+ def set_field(self, artifact_type: str, id: str, version: int, field: str, value) -> None:
234
+ """Persist a single mutable side-channel field on an already-stored artifact doc.
235
+
236
+ Identity is not a side channel: `id`, `version` and `type` address the document being
237
+ updated, so setting one would rename a stored artifact out from under the filter that
238
+ found it — and would reach the store without passing the id reservation.
239
+ """
240
+ if field in _IDENTITY_FIELDS:
241
+ raise ValueError(
242
+ f"set_field cannot change {field!r}: it identifies the document being updated, "
243
+ f"not a side-channel field (identity: {', '.join(sorted(_IDENTITY_FIELDS))})"
244
+ )
245
+ self._doc_store.update(
246
+ ARTIFACTS_COLLECTION,
247
+ Filter.of(id=id, version=version, type=artifact_type),
248
+ UpdateSpec(set={field: value}),
249
+ )
250
+
251
+ def _deserialize(self, doc: dict) -> "Artifact":
252
+ # Remove MongoDB-specific fields not in Pydantic model
253
+ doc.pop("_id", None)
254
+ doc.pop("created_at_utc", None)
255
+
256
+ artifact_type = canonical_type(doc["type"])
257
+ cls = get_artifact_registry().get(artifact_type)
258
+ if cls is None:
259
+ # A plugin that claims the name says why it is missing; the config remedies are for a name none claims.
260
+ if note := _registration.failure_note(_registration.ARTIFACTS, artifact_type):
261
+ raise ValueError(f"Unknown artifact type {doc['type']!r}{note}")
262
+ raise ValueError(
263
+ f"Unknown artifact type {doc['type']!r}; register it under [artifacts].impls "
264
+ f"in .agentenv/config.toml, or map a renamed type to its current spelling "
265
+ f"under [artifacts].type_aliases"
266
+ )
267
+ return cls.model_validate(doc)
268
+
269
+ def execute_query(self, query: ArtifactQuery) -> list["Artifact"]:
270
+ filt, sort = to_document_query(query)
271
+ return self._versioned.query(filt, sort, query._limit_value, query._offset_value)
272
+
273
+ def execute_count(self, query: ArtifactQuery) -> int:
274
+ filt, _ = to_document_query(query)
275
+ return self._versioned.count(filt)
276
+
277
+
278
+ _artifact_store: Optional[ArtifactStore] = None
279
+
280
+
281
+ def get_artifact_store() -> ArtifactStore:
282
+ global _artifact_store
283
+ if _artifact_store is None:
284
+ _artifact_store = ArtifactStore()
285
+ return _artifact_store
286
+
287
+
288
+ def set_artifact_store(store: ArtifactStore) -> None:
289
+ global _artifact_store
290
+ _artifact_store = store
291
+
292
+
293
+ def reset_artifact_store() -> None:
294
+ global _artifact_store
295
+ _artifact_store = None
@@ -0,0 +1,38 @@
1
+ """Universe — abstract base for artifacts that bundle other artifacts together."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import ClassVar
6
+
7
+ from agent_env.artifact.artifact import Artifact
8
+
9
+
10
+ class Universe(Artifact):
11
+ """Abstract base for 'universe' artifacts that bundle other artifacts.
12
+
13
+ Cannot be instantiated directly — use a concrete subclass such as
14
+ ``EnvironmentUniverseArtifact`` or ``FileArtifactUniverse``. A universe is
15
+ itself stored in the artifacts collection (so it benefits from the
16
+ existing versioning / query / registry machinery); its payload is a set
17
+ of references (IDs) to other artifacts.
18
+ """
19
+
20
+ # Marker flipped to False on any subclass that declares a concrete
21
+ # ``type`` literal default (e.g. ``type: Literal["environment_universe"] = "environment_universe"``).
22
+ __is_abstract__: ClassVar[bool] = True
23
+
24
+ def __init_subclass__(cls, **kwargs):
25
+ super().__init_subclass__(**kwargs)
26
+ # A subclass is concrete if its ``type`` field has a non-empty default.
27
+ type_field = cls.model_fields.get("type")
28
+ if type_field is not None and type_field.default not in (None, ""):
29
+ cls.__is_abstract__ = False
30
+
31
+ def __init__(self, **data):
32
+ if self.__class__.__is_abstract__:
33
+ raise TypeError(
34
+ f"{self.__class__.__name__} is abstract and cannot be instantiated; "
35
+ "use a concrete Universe subclass (e.g. EnvironmentUniverseArtifact, "
36
+ "FileArtifactUniverse)."
37
+ )
38
+ super().__init__(**data)
@@ -0,0 +1,41 @@
1
+ """Cost-attribution dimensions carried from the caller down to a sandbox provider.
2
+
3
+ Attribution is an open ``dict[str, str]``. Core threads it without reading it; each provider
4
+ picks out the keys its billing backend understands and ignores the rest, so a deployment can
5
+ attribute on whatever dimensions and terminology it uses.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ Attribution = dict[str, str]
11
+
12
+ # The one key in a step's open ``metadata`` map that is forwarded to compute as attribution.
13
+ ATTRIBUTION_KEY = "attribution"
14
+
15
+
16
+ # Names the pipeline step that deployed a sandbox, as ``<task_id>_<step_id>``, and the run
17
+ # (task instance) it belongs to, so sandbox cost can be broken down per run and per step.
18
+ # Modal stamps both on each sandbox as tags.
19
+ PIPELINE_STEP_KEY = "pipeline_step"
20
+ RUN_ID_KEY = "run_id"
21
+
22
+
23
+ def attribution_of(step) -> Attribution:
24
+ """A step's attribution: a copy of ``metadata["attribution"]`` (``{}`` when unset)."""
25
+ return dict((getattr(step, "metadata", None) or {}).get(ATTRIBUTION_KEY) or {})
26
+
27
+
28
+ def deploy_attribution(step, context) -> Attribution:
29
+ """``attribution_of(step)`` plus ``pipeline_step`` and ``run_id`` when known.
30
+
31
+ ``pipeline_step`` needs ``context.metadata["task_id"]`` (set by the hub worker and the CLI);
32
+ ``run_id`` is ``context.instance_id``. Task-authored values win.
33
+ """
34
+ attribution = attribution_of(step)
35
+ task_id = (getattr(context, "metadata", None) or {}).get("task_id")
36
+ if task_id and PIPELINE_STEP_KEY not in attribution:
37
+ attribution[PIPELINE_STEP_KEY] = f"{task_id}_{step.id}"
38
+ instance_id = getattr(context, "instance_id", None)
39
+ if instance_id and RUN_ID_KEY not in attribution:
40
+ attribution[RUN_ID_KEY] = instance_id
41
+ return attribution
@@ -0,0 +1,4 @@
1
+ """Bundles: folders of entities that run without a registry."""
2
+
3
+ from .parse import Bundle, BundleEntry, BundleError, BundleKind, is_ignored, parse_bundle
4
+ from .run import BundleRun, EvalRun, Outcome, RunInterrupted, TaskRun, run_bundle
@@ -0,0 +1,128 @@
1
+ """Filesystem plumbing for the bundle parser: a path spelled as the filesystem stores it, the user's
2
+ home, a regular file read without surprises, names compared without case, and paths rendered for
3
+ messages."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import errno
8
+ import os
9
+ import stat
10
+ import sys
11
+ import unicodedata
12
+ from pathlib import Path
13
+
14
+ try:
15
+ import fcntl
16
+ import pwd
17
+ except ImportError: # Windows, which agent-env does not target.
18
+ fcntl = pwd = None
19
+
20
+ # Config files are small; the cap keeps a stray large file, or a link to one, from being read whole.
21
+ MAX_FILE_BYTES = 8 << 20
22
+ _UNSHOWN = frozenset({"Cc", "Cf", "Zl", "Zp"})
23
+
24
+
25
+ class Unreadable(Exception):
26
+ pass
27
+
28
+
29
+ def on_disk(path: Path) -> Path:
30
+ """``path`` spelled as the filesystem stores it; a strict resolve keeps the typed case on a
31
+ case-insensitive filesystem. macOS answers with ``F_GETPATH``. Elsewhere each component's
32
+ spelling comes from its parent's listing, where the parent can be listed."""
33
+ if sys.platform == "darwin" and fcntl is not None:
34
+ try:
35
+ fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
36
+ try:
37
+ spelled = fcntl.fcntl(fd, fcntl.F_GETPATH, bytes(1024))
38
+ finally:
39
+ os.close(fd)
40
+ except OSError:
41
+ return path
42
+ return Path(os.fsdecode(spelled.split(b"\0", 1)[0]))
43
+ spelled_path = Path(path.anchor)
44
+ for part in path.parts[1:]:
45
+ try:
46
+ names = os.listdir(spelled_path)
47
+ except OSError:
48
+ names = []
49
+ if part not in names:
50
+ matches = [name for name in names if fold(name) == fold(part)]
51
+ part = matches[0] if len(matches) == 1 else part
52
+ spelled_path /= part
53
+ return spelled_path
54
+
55
+
56
+ def home() -> Path | None:
57
+ """The user's home: ``$HOME`` if it is an absolute folder other than ``/``, else the passwd
58
+ entry's, else none."""
59
+ candidates = [os.environ.get("HOME", "")]
60
+ if pwd is not None:
61
+ try:
62
+ candidates.append(pwd.getpwuid(os.getuid()).pw_dir)
63
+ except KeyError:
64
+ pass
65
+ for candidate in candidates:
66
+ if not candidate.startswith("/"):
67
+ continue
68
+ try:
69
+ resolved = Path(candidate).resolve(strict=True)
70
+ except (OSError, RuntimeError, ValueError):
71
+ continue
72
+ if resolved.is_dir() and resolved != Path(resolved.anchor):
73
+ return on_disk(resolved)
74
+ return None
75
+
76
+
77
+ def read_regular(path: Path) -> bytes:
78
+ """A regular file's bytes, opened without blocking so a FIFO can't hang the parse. Raises
79
+ Unreadable with the reason otherwise."""
80
+ try:
81
+ fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
82
+ except OSError as e:
83
+ raise Unreadable(os_reason(e)) from None
84
+ try:
85
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
86
+ raise Unreadable("not a regular file")
87
+ with os.fdopen(fd, "rb", closefd=False) as f:
88
+ data = f.read(MAX_FILE_BYTES + 1)
89
+ except OSError as e:
90
+ raise Unreadable(os_reason(e)) from None
91
+ finally:
92
+ os.close(fd)
93
+ if len(data) > MAX_FILE_BYTES:
94
+ raise Unreadable(f"larger than the {MAX_FILE_BYTES >> 20} MiB limit for a bundle's config files")
95
+ return data
96
+
97
+
98
+ def fold(name: str) -> str:
99
+ """The canonical caseless form: casefold output isn't always NFC, so normalize both sides."""
100
+ return unicodedata.normalize("NFC", unicodedata.normalize("NFC", name).casefold())
101
+
102
+
103
+ def os_reason(e: OSError) -> str:
104
+ if e.errno == errno.ELOOP:
105
+ return "a symlink loop, or too many links"
106
+ if isinstance(e, PermissionError):
107
+ return "permission denied"
108
+ return e.strerror or str(e)
109
+
110
+
111
+ def with_article(noun: str) -> str:
112
+ return f"{'an' if noun[:1] in 'aeiou' else 'a'} {noun}"
113
+
114
+
115
+ def relative(root: Path, path: Path) -> str:
116
+ """``path`` relative to the bundle's ``root``, for a message."""
117
+ return show(os.path.relpath(path, root))
118
+
119
+
120
+ def show(value) -> str:
121
+ """Text for a message, with undecodable bytes, lone surrogates and control, format and
122
+ line-separator characters escaped, so a problem stays one printable line."""
123
+ text = str(value)
124
+ try:
125
+ text = os.fsencode(text).decode("utf-8", "backslashreplace")
126
+ except UnicodeEncodeError:
127
+ text = text.encode("utf-8", "backslashreplace").decode("utf-8")
128
+ return "".join(ascii(c)[1:-1] if unicodedata.category(c) in _UNSHOWN else c for c in text)