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,415 @@
1
+ """Google Cloud Storage implementation of the ObjectStore interface (the ``gcp`` extra)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import contextlib
7
+ import json
8
+ import logging
9
+ import os
10
+ import stat
11
+ import uuid
12
+ from collections.abc import Callable, Iterator
13
+ from datetime import UTC, datetime, timedelta
14
+ from pathlib import Path
15
+ from typing import BinaryIO
16
+
17
+ import google.auth
18
+ import requests
19
+ from agentenv_protocol.transfers import HttpGetGrant, HttpPostPolicyGrant, HttpPutGrant
20
+ from google.api_core.exceptions import GoogleAPIError, NotFound, PreconditionFailed, RetryError
21
+ from google.auth import iam
22
+ from google.auth.credentials import Signing
23
+ from google.auth.exceptions import GoogleAuthError, TransportError
24
+ from google.cloud import storage
25
+ from google.cloud.storage import transfer_manager
26
+ from google.cloud.storage.retry import DEFAULT_RETRY
27
+ from google.oauth2 import service_account
28
+
29
+ from agent_env.config.errors import ConfigError
30
+ from agent_env.store import _google
31
+ from agent_env.store.base import GrantUnavailableError, ObjectAlreadyExistsError, ObjectNotFoundError
32
+ from agent_env.store.object_store.object_store import (
33
+ DEFAULT_CONTENT_TYPE,
34
+ ObjectMetadata,
35
+ ObjectStore,
36
+ UploadPolicy,
37
+ )
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ _SCHEME = "gs://"
42
+ # A key's V4 signature lasts up to seven days; Google guarantees one made through IAM signBlob,
43
+ # which signs with a system-managed key, for twelve hours.
44
+ _MAX_KEY_SIGNED_SECONDS = 7 * 24 * 60 * 60
45
+ _MAX_IAM_SIGNED_SECONDS = 12 * 60 * 60
46
+ _PROBE_TIMEOUT_SECONDS = 20
47
+ # The client creates a larger object in a resumable session, which meets a taken key only at its
48
+ # last chunk.
49
+ _MULTIPART_LIMIT = 8 * 1024 * 1024
50
+ # Tags each write-once create, so a 412 for a create the client retried after losing its
51
+ # response can be told apart from an object another writer put there first.
52
+ _WRITE_ID = "agentenv-write-id"
53
+ _DOWNLOAD_WORKERS = 4
54
+
55
+
56
+ class _IamSigner(Signing):
57
+ """Signs as a service account through IAM signBlob over one pooled session, retrying dropped
58
+ connections. google-auth's impersonated credentials open a connection per signature."""
59
+
60
+ def __init__(self, credentials, email: str) -> None:
61
+ self._iam = iam.Signer(_google.pooled_request(), credentials, email)
62
+ self._email = email
63
+
64
+ def sign_bytes(self, message: bytes) -> bytes:
65
+ try:
66
+ return _google.RETRY(self._iam.sign)(message)
67
+ except RetryError as e:
68
+ raise TransportError(f"IAM signed nothing as {self._email} within the retry window: {e.cause}") from e
69
+
70
+ @property
71
+ def signer_email(self) -> str:
72
+ return self._email
73
+
74
+ @property
75
+ def signer(self) -> iam.Signer:
76
+ return self._iam
77
+
78
+
79
+ class GcsObjectStore(ObjectStore):
80
+ """ObjectStore backed by a Cloud Storage bucket (injected client, config-free).
81
+
82
+ ``signer`` signs URLs and grants: credentials that hold a key, or ones that sign through IAM.
83
+ Without one the store issues neither. Its reads return an object's bytes as stored."""
84
+
85
+ max_single_upload_bytes = 5 * 1024**4 # one Cloud Storage object
86
+
87
+ def __init__(self, client, bucket: str, *, signer: Signing | None = None) -> None:
88
+ self._client = client
89
+ self._bucket = bucket
90
+ self._signer = signer
91
+ # Grants are HTTPS URLs, so a plain-HTTP endpoint (a local emulator) issues none.
92
+ self.supports_transfer_grants = signer is not None and client.api_endpoint.startswith("https://")
93
+ self._max_signed_seconds = (
94
+ _MAX_KEY_SIGNED_SECONDS if isinstance(signer, service_account.Credentials) else _MAX_IAM_SIGNED_SECONDS
95
+ )
96
+ self._warned_unsigned = False
97
+
98
+ @classmethod
99
+ def from_config(
100
+ cls, *, bucket: str, project: str | None = None, signing_service_account: str | None = None
101
+ ) -> GcsObjectStore:
102
+ """Authenticate through Application Default Credentials. With ``signing_service_account``
103
+ the store always signs as that account through IAM, which needs
104
+ ``iam.serviceAccounts.signBlob`` on it; without it, credentials that can sign do. Lists
105
+ the bucket, and signs once when IAM signs, so a missing bucket or a signer that cannot sign
106
+ fails here rather than at the first read or signed URL."""
107
+ if signing_service_account is not None and not signing_service_account.strip():
108
+ raise ConfigError("signing_service_account is empty; omit it to sign with the credentials themselves")
109
+ try:
110
+ credentials, default_project = google.auth.default(scopes=_google.SCOPES)
111
+ except (GoogleAuthError, OSError) as e:
112
+ raise ConfigError(f"No Google credentials for gs://{bucket}: {e}") from e
113
+ client = storage.Client(project=project or default_project, credentials=credentials)
114
+ if signing_service_account:
115
+ signer = _IamSigner(credentials, signing_service_account)
116
+ else:
117
+ signer = credentials if isinstance(credentials, Signing) else None
118
+ probe = DEFAULT_RETRY.with_timeout(_PROBE_TIMEOUT_SECONDS)
119
+ try:
120
+ next(iter(client.list_blobs(bucket, max_results=1, timeout=_PROBE_TIMEOUT_SECONDS, retry=probe)), None)
121
+ except (GoogleAPIError, GoogleAuthError, requests.RequestException, ValueError) as e:
122
+ raise ConfigError(f"Cannot list gs://{bucket}: {e}") from e
123
+ if signer is not None and not isinstance(signer, service_account.Credentials):
124
+ try:
125
+ signer.sign_bytes(b"agent-env signing check")
126
+ except (GoogleAPIError, GoogleAuthError, requests.RequestException) as e:
127
+ quota = getattr(credentials, "quota_project_id", None)
128
+ billed = f" (requests are billed to quota project {quota!r})" if quota else ""
129
+ raise ConfigError(f"Cannot sign as {signer.signer_email}{billed}: {e}") from e
130
+ return cls(client, bucket, signer=signer)
131
+
132
+ def put(
133
+ self,
134
+ key: str,
135
+ data: bytes,
136
+ content_type: str = DEFAULT_CONTENT_TYPE,
137
+ allow_overwrite: bool = False,
138
+ ) -> str:
139
+ url = self.object_url(key)
140
+ logger.info("Uploading %d bytes to %s", len(data), url)
141
+ blob = self._blob(url)
142
+ if allow_overwrite:
143
+ blob.upload_from_string(data, content_type=content_type)
144
+ else:
145
+ self._create(url, blob, lambda: blob.upload_from_string(
146
+ data, content_type=content_type, if_generation_match=0
147
+ ))
148
+ return url
149
+
150
+ def put_file(self, key: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
151
+ return self._put_file(self.object_url(key), file_path, content_type)
152
+
153
+ def put_file_at(self, object_url: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
154
+ return self._put_file(object_url, file_path, content_type)
155
+
156
+ def get(self, object_url: str) -> bytes:
157
+ logger.info("Downloading from %s", object_url)
158
+ with _missing_as_not_found(object_url):
159
+ return self._blob(object_url).download_as_bytes(raw_download=True)
160
+
161
+ def download_to_file(self, object_url: str, dest_path: str) -> None:
162
+ """Fetch ranges concurrently, each retried on its own, check the whole object's crc32c,
163
+ and rename into place: a failed download leaves neither a partial file nor a changed
164
+ destination, and a replaced destination keeps its permissions."""
165
+ logger.info("Streaming %s to %s", object_url, dest_path)
166
+ blob = self._blob(object_url)
167
+ dest = Path(dest_path)
168
+ dest.parent.mkdir(parents=True, exist_ok=True)
169
+ partial = dest.with_name(f".{uuid.uuid4().hex[:16]}.part")
170
+ try:
171
+ with _missing_as_not_found(object_url):
172
+ transfer_manager.download_chunks_concurrently(
173
+ blob,
174
+ str(partial),
175
+ download_kwargs={"raw_download": True},
176
+ worker_type=transfer_manager.THREAD,
177
+ max_workers=_DOWNLOAD_WORKERS,
178
+ )
179
+ with contextlib.suppress(FileNotFoundError):
180
+ os.chmod(partial, stat.S_IMODE(dest.stat().st_mode))
181
+ os.replace(partial, dest)
182
+ finally:
183
+ partial.unlink(missing_ok=True)
184
+
185
+ def open(self, object_url: str) -> BinaryIO:
186
+ blob = self._existing_blob(object_url)
187
+ if blob is None:
188
+ raise ObjectNotFoundError(f"No object at {object_url}.")
189
+ return blob.open("rb", raw_download=True)
190
+
191
+ def get_object_metadata(self, key: str) -> ObjectMetadata | None:
192
+ return self.get_object_metadata_at(self.object_url(key))
193
+
194
+ def get_object_metadata_at(self, object_url: str) -> ObjectMetadata | None:
195
+ blob = self._existing_blob(object_url)
196
+ if blob is None:
197
+ return None
198
+ return ObjectMetadata(
199
+ content_type=blob.content_type,
200
+ size=blob.size,
201
+ last_modified=blob.updated,
202
+ content_encoding=blob.content_encoding,
203
+ )
204
+
205
+ def list(self, prefix: str) -> list[str]:
206
+ return self._list_keys(self._bucket, prefix)
207
+
208
+ def list_at(self, url_prefix: str) -> list[str]:
209
+ bucket, prefix = _split(url_prefix)
210
+ return [_url(bucket, key) for key in self._list_keys(bucket, prefix)]
211
+
212
+ def object_url(self, key: str) -> str:
213
+ return _url(self._bucket, key)
214
+
215
+ def get_object_key(self, object_url: str) -> str:
216
+ bucket, key = _split(object_url)
217
+ if bucket != self._bucket:
218
+ raise ValueError(f"{object_url!r} is not an object in gs://{self._bucket}.")
219
+ return key
220
+
221
+ def signed_get_url(self, object_url: str, expires_in: int = 3600) -> str | None:
222
+ blob = self._blob(object_url)
223
+ if not self._can_sign():
224
+ return None
225
+ return self._sign(blob, "GET", min(expires_in, self._max_signed_seconds))
226
+
227
+ def signed_put_url(self, object_url: str, expires_in: int = 3600) -> str | None:
228
+ blob = self._blob(object_url)
229
+ if not self._can_sign():
230
+ return None
231
+ return self._sign(blob, "PUT", min(expires_in, self._max_signed_seconds))
232
+
233
+ def issue_read_grant(self, object_url: str, *, expires_in: int = 3600) -> HttpGetGrant:
234
+ blob = self._blob(object_url)
235
+ expires_at = self._grant_expiry(expires_in)
236
+ return HttpGetGrant(kind="http-get", url=self._sign(blob, "GET", expires_in), expires_at=expires_at)
237
+
238
+ def issue_write_grant(
239
+ self,
240
+ object_url: str,
241
+ *,
242
+ media_type: str,
243
+ max_bytes: int,
244
+ expires_in: int = 3600,
245
+ ) -> HttpPutGrant:
246
+ blob = self._blob(object_url)
247
+ expires_at = self._grant_expiry(expires_in)
248
+ bound = {"x-goog-content-length-range": f"0,{max_bytes}"}
249
+ return HttpPutGrant(
250
+ kind="http-put",
251
+ url=self._sign(blob, "PUT", expires_in, content_type=media_type, headers=dict(bound)),
252
+ expires_at=expires_at,
253
+ headers={"Content-Type": media_type, **bound},
254
+ )
255
+
256
+ def issue_upload_policy(
257
+ self, prefix_url: str, *, max_object_bytes: int, expires_in: int
258
+ ) -> UploadPolicy:
259
+ """A V4 POST policy for any key below ``prefix_url``. Its signature lasts all of
260
+ ``expires_in`` up to the signer's cap whatever the caller's own token has left, so it is
261
+ refused only without a signer or past that cap."""
262
+ expires_at = self._grant_expiry(expires_in)
263
+ post = self._post_policy(prefix_url, expires_in, max_object_bytes)
264
+ return UploadPolicy(
265
+ write=HttpPostPolicyGrant(
266
+ kind="http-post-policy",
267
+ url=post["url"],
268
+ fields=post["fields"],
269
+ path_field="key",
270
+ file_field="file",
271
+ ),
272
+ expires_at=expires_at,
273
+ )
274
+
275
+ def signed_post(
276
+ self, url_prefix: str, *, expires_in: int = 3600, max_bytes: int | None = None
277
+ ) -> dict | None:
278
+ if not self._can_sign():
279
+ return None
280
+ return self._post_policy(url_prefix, min(expires_in, self._max_signed_seconds), max_bytes)
281
+
282
+ def _post_policy(self, url_prefix: str, expires_in: int, max_bytes: int | None) -> dict:
283
+ """Built by hand: the client library's helper adds an exact-key condition, which would
284
+ confine the policy to one object. The prefix ends in ``/``, so that ``root`` does not
285
+ admit ``root-evil/``."""
286
+ bucket, prefix = _split(url_prefix.rstrip("/") + "/")
287
+ now = datetime.now(UTC).replace(microsecond=0)
288
+ timestamp = now.strftime("%Y%m%dT%H%M%SZ")
289
+ credential = f"{self._signer.signer_email}/{now:%Y%m%d}/auto/storage/goog4_request"
290
+ conditions: list = [["starts-with", "$key", prefix], ["starts-with", "$Content-Type", ""]]
291
+ if max_bytes is not None:
292
+ conditions.append(["content-length-range", 0, max_bytes])
293
+ conditions += [
294
+ {"bucket": bucket},
295
+ {"x-goog-date": timestamp},
296
+ {"x-goog-credential": credential},
297
+ {"x-goog-algorithm": "GOOG4-RSA-SHA256"},
298
+ ]
299
+ expiration = (now + timedelta(seconds=expires_in)).strftime("%Y-%m-%dT%H:%M:%SZ")
300
+ document = json.dumps({"conditions": conditions, "expiration": expiration}, separators=(",", ":"))
301
+ policy = base64.b64encode(document.encode())
302
+ return {
303
+ "url": f"{self._client.api_endpoint}/{bucket}/",
304
+ "fields": {
305
+ "key": f"{prefix}${{filename}}",
306
+ "policy": policy.decode(),
307
+ "x-goog-algorithm": "GOOG4-RSA-SHA256",
308
+ "x-goog-credential": credential,
309
+ "x-goog-date": timestamp,
310
+ "x-goog-signature": self._signer.sign_bytes(policy).hex(),
311
+ },
312
+ }
313
+
314
+ def _put_file(self, object_url: str, file_path: str, content_type: str) -> str:
315
+ logger.info("Uploading %d bytes from %s to %s", os.path.getsize(file_path), file_path, object_url)
316
+ if os.path.getsize(file_path) > _MULTIPART_LIMIT and self._existing_blob(object_url) is not None:
317
+ raise ObjectAlreadyExistsError(f"Object already exists at {object_url}.")
318
+ blob = self._blob(object_url)
319
+ self._create(object_url, blob, lambda: blob.upload_from_filename(
320
+ file_path, content_type=content_type, if_generation_match=0
321
+ ))
322
+ return object_url
323
+
324
+ def _create(self, object_url: str, blob, upload: Callable[[], None]) -> None:
325
+ write_id = uuid.uuid4().hex
326
+ blob.metadata = {_WRITE_ID: write_id}
327
+ try:
328
+ upload()
329
+ except PreconditionFailed as e:
330
+ existing = self._existing_blob(object_url)
331
+ if existing is None or (existing.metadata or {}).get(_WRITE_ID) != write_id:
332
+ raise ObjectAlreadyExistsError(f"Object already exists at {object_url}.") from e
333
+
334
+ def _blob(self, object_url: str):
335
+ bucket, key = _split_object(object_url)
336
+ return self._client.bucket(bucket).blob(key)
337
+
338
+ def _existing_blob(self, object_url: str):
339
+ bucket, key = _split_object(object_url)
340
+ return self._client.bucket(bucket).get_blob(key)
341
+
342
+ def _list_keys(self, bucket: str, prefix: str) -> list[str]:
343
+ blobs = self._client.list_blobs(bucket, prefix=prefix, fields="items(name),nextPageToken")
344
+ return [blob.name for blob in blobs if not blob.name.endswith("/")]
345
+
346
+ def _can_sign(self) -> bool:
347
+ if self._signer is None and not self._warned_unsigned:
348
+ logger.warning(
349
+ "GcsObjectStore has no signer, so it issues no signed URLs; set "
350
+ "signing_service_account, or run with credentials that can sign"
351
+ )
352
+ self._warned_unsigned = True
353
+ return self._signer is not None
354
+
355
+ def _grant_expiry(self, expires_in: int) -> datetime:
356
+ if self._signer is None:
357
+ raise GrantUnavailableError(
358
+ "GcsObjectStore has no signer; set signing_service_account to issue grants"
359
+ )
360
+ if expires_in > self._max_signed_seconds:
361
+ raise GrantUnavailableError(
362
+ f"GcsObjectStore's signer signs grants for at most {self._max_signed_seconds}s; "
363
+ f"{expires_in}s was requested"
364
+ )
365
+ # The signature's start is stamped to the second, after this.
366
+ return datetime.now(UTC).replace(microsecond=0) + timedelta(seconds=expires_in)
367
+
368
+ def _sign(
369
+ self,
370
+ blob,
371
+ method: str,
372
+ expires_in: int,
373
+ *,
374
+ content_type: str | None = None,
375
+ headers: dict[str, str] | None = None,
376
+ ) -> str:
377
+ return blob.generate_signed_url(
378
+ version="v4",
379
+ expiration=timedelta(seconds=expires_in),
380
+ method=method,
381
+ content_type=content_type,
382
+ headers=headers,
383
+ credentials=self._signer,
384
+ )
385
+
386
+
387
+ def _url(bucket: str, key: str) -> str:
388
+ return f"{_SCHEME}{bucket}/{key}"
389
+
390
+
391
+ def _split(object_url: str) -> tuple[str, str]:
392
+ # Not urlparse: it would end the key at a "#" or "?", which object names may contain.
393
+ if not object_url.startswith(_SCHEME):
394
+ raise ValueError(f"{object_url!r} is not a gs:// object url.")
395
+ bucket, _, key = object_url[len(_SCHEME):].partition("/")
396
+ if not bucket:
397
+ raise ValueError(f"{object_url!r} names no bucket.")
398
+ return bucket, key
399
+
400
+
401
+ def _split_object(object_url: str) -> tuple[str, str]:
402
+ """An object's url, which unlike a prefix's names a key: a signed url for an empty one
403
+ would list the bucket."""
404
+ bucket, key = _split(object_url)
405
+ if not key:
406
+ raise ValueError(f"{object_url!r} names no object.")
407
+ return bucket, key
408
+
409
+
410
+ @contextlib.contextmanager
411
+ def _missing_as_not_found(object_url: str) -> Iterator[None]:
412
+ try:
413
+ yield
414
+ except NotFound as e:
415
+ raise ObjectNotFoundError(f"No object at {object_url}.") from e
@@ -0,0 +1,143 @@
1
+ """Local filesystem implementation of the ObjectStore interface (no infra)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import shutil
7
+ from collections.abc import Iterator
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import BinaryIO
11
+
12
+ from agent_env.store.base import ObjectAlreadyExistsError, ObjectNotFoundError
13
+ from agent_env.store.local_state import ensure_state_dir
14
+ from agent_env.store.object_store.object_store import DEFAULT_CONTENT_TYPE, ObjectMetadata, ObjectStore
15
+
16
+
17
+ class LocalFilesystemObjectStore(ObjectStore):
18
+ """ObjectStore backed by a local directory tree; content types are not persisted.
19
+
20
+ The root-level ``.gitignore`` key is reserved for the ignore rule the store writes."""
21
+
22
+ def __init__(self, root: str) -> None:
23
+ self._root = Path(root)
24
+
25
+ @property
26
+ def root(self) -> Path:
27
+ return self._root
28
+
29
+ def put(
30
+ self,
31
+ key: str,
32
+ data: bytes,
33
+ content_type: str = DEFAULT_CONTENT_TYPE,
34
+ allow_overwrite: bool = False,
35
+ ) -> str:
36
+ path = self._resolve(key)
37
+ if path.exists() and not allow_overwrite:
38
+ raise ObjectAlreadyExistsError(f"Object already exists at {path}.")
39
+ ensure_state_dir(self._root)
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ path.write_bytes(data)
42
+ return self._to_url(path)
43
+
44
+ def put_file(self, key: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
45
+ path = self._resolve(key)
46
+ if path.exists():
47
+ raise ObjectAlreadyExistsError(f"Object already exists at {path}.")
48
+ ensure_state_dir(self._root)
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ shutil.copyfile(file_path, path)
51
+ return self._to_url(path)
52
+
53
+ def put_file_at(self, object_url: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
54
+ return self.put_file(self.get_object_key(object_url), file_path, content_type)
55
+
56
+ def get(self, object_url: str) -> bytes:
57
+ with _missing_as_not_found(object_url):
58
+ return Path(self._from_url(object_url)).read_bytes()
59
+
60
+ def download_to_file(self, object_url: str, dest_path: str) -> None:
61
+ source = self._from_url(object_url)
62
+ dest = Path(dest_path)
63
+ dest.parent.mkdir(parents=True, exist_ok=True)
64
+ try:
65
+ shutil.copyfile(source, dest)
66
+ except (FileNotFoundError, IsADirectoryError, NotADirectoryError) as e:
67
+ if e.filename != source:
68
+ raise
69
+ raise ObjectNotFoundError(f"No object at {object_url}.") from e
70
+
71
+ def open(self, object_url: str) -> BinaryIO:
72
+ with _missing_as_not_found(object_url):
73
+ return Path(self._from_url(object_url)).open("rb")
74
+
75
+ def get_object_metadata(self, key: str) -> ObjectMetadata | None:
76
+ path = self._resolve(key)
77
+ if not path.is_file():
78
+ return None
79
+ st = path.stat()
80
+ return ObjectMetadata(content_type=None, size=st.st_size, last_modified=datetime.fromtimestamp(st.st_mtime, tz=timezone.utc))
81
+
82
+ def get_object_metadata_at(self, object_url: str) -> ObjectMetadata | None:
83
+ return self.get_object_metadata(self.get_object_key(object_url))
84
+
85
+ def list(self, prefix: str) -> list[str]:
86
+ keys = []
87
+ for path in self._root.rglob("*"):
88
+ if not path.is_file() or path == self._root / ".gitignore":
89
+ continue
90
+ key = path.relative_to(self._root).as_posix()
91
+ if key.startswith(prefix):
92
+ keys.append(key)
93
+ return keys
94
+
95
+ def list_at(self, url_prefix: str) -> list[str]:
96
+ base = url_prefix if url_prefix.endswith("/") else url_prefix + "/"
97
+ dir_path = self._resolve(self.get_object_key(url_prefix))
98
+ if not dir_path.is_dir():
99
+ return []
100
+ return [
101
+ base + p.relative_to(dir_path).as_posix()
102
+ for p in sorted(dir_path.rglob("*"))
103
+ if p.is_file() and p != self._root.resolve() / ".gitignore"
104
+ ]
105
+
106
+ def object_url(self, key: str) -> str:
107
+ return self._to_url(self._resolve(key))
108
+
109
+ def get_object_key(self, object_url: str) -> str:
110
+ if not object_url.startswith("file://"):
111
+ raise ValueError(f"{object_url!r} is not a file:// url of this store.")
112
+ root = self._root.resolve()
113
+ try:
114
+ path = Path(object_url[len("file://"):]).resolve()
115
+ except (OSError, RuntimeError) as e:
116
+ raise ValueError(f"{object_url!r} is not an object in {root}.") from e
117
+ if path != root and root not in path.parents:
118
+ raise ValueError(f"{object_url!r} is not an object in {root}.")
119
+ return path.relative_to(root).as_posix()
120
+
121
+ def _resolve(self, key: str) -> Path:
122
+ resolved = (self._root / key).resolve()
123
+ if not resolved.is_relative_to(self._root.resolve()):
124
+ raise ValueError(f"Key {key!r} escapes the store root.")
125
+ if resolved == self._root.resolve() / ".gitignore":
126
+ raise ValueError(f"Key {key!r} is reserved: the store keeps its .gitignore there.")
127
+ return resolved
128
+
129
+ @staticmethod
130
+ def _to_url(path: Path) -> str:
131
+ return f"file://{path.resolve()}"
132
+
133
+ @staticmethod
134
+ def _from_url(object_url: str) -> str:
135
+ return object_url[len("file://"):] if object_url.startswith("file://") else object_url
136
+
137
+
138
+ @contextlib.contextmanager
139
+ def _missing_as_not_found(object_url: str) -> Iterator[None]:
140
+ try:
141
+ yield
142
+ except (FileNotFoundError, IsADirectoryError, NotADirectoryError) as e:
143
+ raise ObjectNotFoundError(f"No object at {object_url}.") from e