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,206 @@
1
+ """SkillArtifact — Agent Skills (agentskills.io) as first-class artifacts.
2
+
3
+ A SkillArtifact references one FileArtifactUniverse bundling every file in the skill
4
+ (SKILL.md + scripts/, references/, assets/, …) colocated under a single object-store prefix
5
+ (`artifacts/skill/<key_segment(id)>/<version>/`). That prefix IS the bundle — no duplication, and
6
+ the prefix is handed to deployed A2A agents as `skill_s3_url`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import re
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING, Iterator, Literal, Optional
16
+
17
+ import yaml
18
+ from pydantic import Field
19
+
20
+ from agent_env.artifact.artifact import Artifact
21
+ from agent_env.config import get_config
22
+ from agent_env.store.base import ObjectNotFoundError
23
+ from agent_env.store.ids import derive_id, key_segment
24
+
25
+ if TYPE_CHECKING:
26
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
27
+
28
+
29
+ AGENT_SKILLS_SPEC_VERSION = "0.1"
30
+
31
+ _SKILL_NAME_RE = re.compile(r"^(?!-)(?!.*--)[a-z0-9-]{1,64}(?<!-)$")
32
+ _SKILL_MD_FILENAME = "SKILL.md"
33
+ _MAX_DESCRIPTION_LEN = 1024
34
+ _MAX_COMPATIBILITY_LEN = 500
35
+
36
+
37
+ class SkillArtifact(Artifact):
38
+ type: Literal["skill"] = "skill"
39
+
40
+ skill_files_id: str = Field(description="ID of the FileArtifactUniverse bundling every file in the skill")
41
+ skill_object_url: str = Field(alias="skill_s3_url", description="Object-store prefix where the skill bundle lives (handed to agents)")
42
+ agent_skills_spec_version: str = Field(description="agentskills.io spec revision this skill was validated under")
43
+
44
+ skill_name: str = Field(description="Skill name from SKILL.md frontmatter; equals the artifact id")
45
+ description: str = Field(description="Skill description from SKILL.md frontmatter")
46
+ license: Optional[str] = Field(default=None)
47
+ compatibility: Optional[str] = Field(default=None)
48
+ allowed_tools: Optional[str] = Field(default=None)
49
+ skill_metadata: Optional[dict[str, str]] = Field(default=None)
50
+
51
+ @classmethod
52
+ def validate(
53
+ cls,
54
+ *,
55
+ skill_md: Optional[bytes] = None,
56
+ s3_url: Optional[str] = None,
57
+ expected_name: str,
58
+ ) -> None:
59
+ if (skill_md is None) == (s3_url is None):
60
+ raise ValueError("specify exactly one of skill_md or s3_url")
61
+ if s3_url is not None:
62
+ skill_md = _fetch_skill_md(s3_url)
63
+ frontmatter, _body = _parse_skill_md(skill_md)
64
+ _validate_frontmatter(frontmatter, expected_name=expected_name)
65
+
66
+ @classmethod
67
+ def put(cls, id: str, *, skill_dir: Path) -> "SkillArtifact":
68
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
69
+ from agent_env.artifact.store import get_artifact_store
70
+
71
+ skill_md_path = skill_dir / _SKILL_MD_FILENAME
72
+ if not skill_md_path.is_file():
73
+ raise ValueError(f"{_SKILL_MD_FILENAME} not found at {skill_md_path}")
74
+
75
+ skill_md_bytes = skill_md_path.read_bytes()
76
+ cls.validate(skill_md=skill_md_bytes, expected_name=id)
77
+ frontmatter, _body = _parse_skill_md(skill_md_bytes)
78
+
79
+ files: dict[str, Path] = {}
80
+ for p in sorted(skill_dir.rglob("*")):
81
+ if p.is_file():
82
+ files[p.relative_to(skill_dir).as_posix()] = p
83
+
84
+ store = get_artifact_store()
85
+ version = store.next_version(id)
86
+ key = f"{get_config().get_artifact_key_prefix()}artifacts/skill/{key_segment(id)}/{version}/"
87
+ skill_s3_url = get_config().get_object_store_for(id).object_url(key)
88
+
89
+ universe = FileArtifactUniverse.put_bundled(
90
+ id=derive_id(id, "files"),
91
+ files=files,
92
+ s3_url=skill_s3_url,
93
+ )
94
+
95
+ instance = cls(
96
+ id=id,
97
+ version=version,
98
+ skill_files_id=universe.id,
99
+ skill_s3_url=skill_s3_url,
100
+ agent_skills_spec_version=AGENT_SKILLS_SPEC_VERSION,
101
+ skill_name=frontmatter["name"],
102
+ description=frontmatter["description"],
103
+ license=frontmatter.get("license"),
104
+ compatibility=frontmatter.get("compatibility"),
105
+ allowed_tools=frontmatter.get("allowed-tools"),
106
+ skill_metadata=frontmatter.get("metadata"),
107
+ )
108
+ return store.put_document(instance)
109
+
110
+ def get_skill_files(self) -> "FileArtifactUniverse":
111
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
112
+ return FileArtifactUniverse.get(self.skill_files_id)
113
+
114
+
115
+ def _parse_skill_md(data: bytes) -> tuple[dict, str]:
116
+ text = data.decode("utf-8")
117
+ if not text.startswith("---"):
118
+ raise ValueError("SKILL.md must begin with a YAML frontmatter block delimited by '---'")
119
+ after_open = text[3:]
120
+ if after_open.startswith("\n"):
121
+ after_open = after_open[1:]
122
+ end_match = re.search(r"^---\s*$", after_open, flags=re.MULTILINE)
123
+ if end_match is None:
124
+ raise ValueError("SKILL.md frontmatter is not closed with '---'")
125
+ frontmatter_text = after_open[: end_match.start()]
126
+ body = after_open[end_match.end():].lstrip("\n")
127
+ parsed = yaml.safe_load(frontmatter_text) or {}
128
+ if not isinstance(parsed, dict):
129
+ raise ValueError("SKILL.md frontmatter must be a YAML mapping")
130
+ return parsed, body
131
+
132
+
133
+ def _validate_frontmatter(fm: dict, expected_name: str) -> None:
134
+ name = fm.get("name")
135
+ if not isinstance(name, str) or not name:
136
+ raise ValueError("SKILL.md frontmatter 'name' is required and must be a non-empty string")
137
+ if not _SKILL_NAME_RE.match(name):
138
+ raise ValueError(
139
+ f"SKILL.md frontmatter 'name' {name!r} does not match the spec: 1-64 chars, lowercase "
140
+ "alphanumerics and hyphens only, no leading/trailing or consecutive hyphens"
141
+ )
142
+ if name != expected_name:
143
+ raise ValueError(
144
+ f"SKILL.md frontmatter 'name' {name!r} must equal the SkillArtifact id {expected_name!r}"
145
+ )
146
+
147
+ description = fm.get("description")
148
+ if not isinstance(description, str) or not description:
149
+ raise ValueError("SKILL.md frontmatter 'description' is required and must be a non-empty string")
150
+ if len(description) > _MAX_DESCRIPTION_LEN:
151
+ raise ValueError(
152
+ f"SKILL.md frontmatter 'description' is {len(description)} chars; max {_MAX_DESCRIPTION_LEN}"
153
+ )
154
+
155
+ compatibility = fm.get("compatibility")
156
+ if compatibility is not None:
157
+ if not isinstance(compatibility, str) or not compatibility:
158
+ raise ValueError("SKILL.md frontmatter 'compatibility' must be a non-empty string if present")
159
+ if len(compatibility) > _MAX_COMPATIBILITY_LEN:
160
+ raise ValueError(
161
+ f"SKILL.md frontmatter 'compatibility' is {len(compatibility)} chars; max {_MAX_COMPATIBILITY_LEN}"
162
+ )
163
+
164
+ license_ = fm.get("license")
165
+ if license_ is not None and (not isinstance(license_, str) or not license_):
166
+ raise ValueError("SKILL.md frontmatter 'license' must be a non-empty string if present")
167
+
168
+ allowed_tools = fm.get("allowed-tools")
169
+ if allowed_tools is not None and (not isinstance(allowed_tools, str) or not allowed_tools):
170
+ raise ValueError("SKILL.md frontmatter 'allowed-tools' must be a non-empty string if present")
171
+
172
+ metadata = fm.get("metadata")
173
+ if metadata is not None:
174
+ if not isinstance(metadata, dict):
175
+ raise ValueError("SKILL.md frontmatter 'metadata' must be a mapping if present")
176
+ for k, v in metadata.items():
177
+ if not isinstance(k, str) or not isinstance(v, str):
178
+ raise ValueError("SKILL.md frontmatter 'metadata' must map strings to strings")
179
+
180
+
181
+ def _fetch_skill_md(object_url: str) -> bytes:
182
+ prefix = object_url if object_url.endswith("/") else object_url + "/"
183
+ skill_md_url = prefix + _SKILL_MD_FILENAME
184
+ try:
185
+ return get_config().get_object_store_at(skill_md_url).get(skill_md_url)
186
+ except ObjectNotFoundError as e:
187
+ raise ValueError(f"{_SKILL_MD_FILENAME} not found at {object_url}") from e
188
+
189
+
190
+ @contextlib.contextmanager
191
+ def download_skill(object_url: str) -> Iterator[Path]:
192
+ """Download an object-store prefix into a local tempdir; yields the dir, cleans up on exit."""
193
+ store = get_config().get_object_store_at(object_url)
194
+ prefix = object_url if object_url.endswith("/") else object_url + "/"
195
+ with tempfile.TemporaryDirectory() as tmp:
196
+ tmp_path = Path(tmp)
197
+ count = 0
198
+ for obj_url in store.list_at(prefix):
199
+ rel = obj_url[len(prefix):]
200
+ if not rel or obj_url.endswith("/"):
201
+ continue
202
+ store.download_to_file(obj_url, str(tmp_path / rel))
203
+ count += 1
204
+ if count == 0:
205
+ raise ValueError(f"No objects found under {object_url}")
206
+ yield tmp_path
@@ -0,0 +1,95 @@
1
+ """VM image artifact for referencing pre-built VM images."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, Optional
6
+
7
+ from pydantic import Field, model_validator
8
+
9
+ from agent_env.artifact.artifact import Artifact
10
+
11
+
12
+ class VMImageArtifact(Artifact):
13
+ """A VM image artifact that references a pre-built VM image.
14
+
15
+ Supports ECR containerdisk images (Ubuntu/Windows) and non-ECR images
16
+ (e.g. macOS). At least one of ecr_url or image_name
17
+ must be provided.
18
+
19
+ Unlike DockerImageArtifact, this does not upload anything to S3.
20
+
21
+ Examples:
22
+ # ECR-based (Ubuntu/Windows)
23
+ VMImageArtifact.put(id="ubuntu-vm", description="...", ecr_url="<account>.dkr.ecr.<region>.amazonaws.com/ubuntu-vm:1.0.4")
24
+
25
+ # Non-ECR (macOS)
26
+ VMImageArtifact.put(id="macos-vm", description="...", image_name="macos-base", os="macos")
27
+ """
28
+
29
+ type: Literal["vm_image"] = "vm_image"
30
+ description: str = Field(description="Human-readable description of the VM image")
31
+ ecr_url: Optional[str] = Field(default=None, description="Full ECR URL for the containerdisk image")
32
+ image_name: Optional[str] = Field(default=None, description="Image name for non-ECR registries")
33
+ os: Optional[str] = Field(default=None, description="OS identifier (e.g. 'macos') — only needed when the sandbox API requires it")
34
+ disk_size_gb: Optional[float] = Field(
35
+ default=None,
36
+ description=(
37
+ "Root disk size in GiB for this VM image. MUST be >= the disk size the image was "
38
+ "baked with; if it is smaller the guest cannot find its root filesystem and drops "
39
+ "to the initramfs shell (\"Gave up waiting for root file system device\" / "
40
+ "\"ALERT! UUID=... does not exist\"). Keep this in sync with the image build. "
41
+ "None -> falls back to the deploying env's default."
42
+ ),
43
+ )
44
+ cpu: Optional[float] = Field(default=None, description="vCPU count for the VM; None -> falls back to the deploying env's default")
45
+ memory_mb: Optional[int] = Field(default=None, description="Memory in MiB for the VM; None -> falls back to the deploying env's default")
46
+ sandbox_type: Optional[str] = Field(
47
+ default=None,
48
+ description=(
49
+ "Sandbox backend this image deploys to (e.g. 'modal_vm', or a name from [sandbox.providers])"
50
+ ),
51
+ )
52
+
53
+ @model_validator(mode="after")
54
+ def validate_image_source(self):
55
+ if not self.ecr_url and not self.image_name:
56
+ raise ValueError("At least one of ecr_url or image_name must be provided")
57
+ return self
58
+
59
+ @property
60
+ def image(self) -> str:
61
+ """Return the image reference (ECR URL or image name)."""
62
+ return self.ecr_url or self.image_name
63
+
64
+ @classmethod
65
+ def put(
66
+ cls,
67
+ id: str,
68
+ *,
69
+ description: str,
70
+ ecr_url: Optional[str] = None,
71
+ image_name: Optional[str] = None,
72
+ os: Optional[str] = None,
73
+ disk_size_gb: Optional[float] = None,
74
+ cpu: Optional[float] = None,
75
+ memory_mb: Optional[int] = None,
76
+ sandbox_type: Optional[str] = None,
77
+ ) -> "VMImageArtifact":
78
+ from agent_env.artifact.store import get_artifact_store
79
+
80
+ store = get_artifact_store()
81
+ version = store.next_version(id)
82
+
83
+ instance = cls(
84
+ id=id,
85
+ version=version,
86
+ description=description,
87
+ ecr_url=ecr_url,
88
+ image_name=image_name,
89
+ os=os,
90
+ disk_size_gb=disk_size_gb,
91
+ cpu=cpu,
92
+ memory_mb=memory_mb,
93
+ sandbox_type=sandbox_type,
94
+ )
95
+ return store.put_document(instance)
@@ -0,0 +1,15 @@
1
+ """ArtifactRef — a pinned reference to a specific (id, version) of an artifact."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class ArtifactRef(BaseModel):
9
+ model_config = ConfigDict(frozen=True)
10
+
11
+ id: str = Field(min_length=1, description="Artifact id")
12
+ version: int = Field(ge=1, description="Pinned artifact version")
13
+
14
+ def __str__(self) -> str:
15
+ return f"{self.id}:{self.version}"
@@ -0,0 +1,251 @@
1
+ """Artifact registry for AgentEnv.
2
+ """
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from collections.abc import Mapping
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from pydantic import TypeAdapter, ValidationError
11
+
12
+ from agent_env.plugins import _registration, _report
13
+ from agent_env.artifact.artifact import Artifact
14
+ from agent_env.artifact.artifacts.cli import CliArtifact
15
+ from agent_env.artifact.artifacts.docker_image import DockerImageArtifact
16
+ from agent_env.artifact.artifacts.file import FileArtifact
17
+ from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
18
+ from agent_env.artifact.artifacts.environment import EnvironmentArtifact
19
+ from agent_env.artifact.artifacts.environment_universe import EnvironmentUniverseArtifact
20
+ from agent_env.artifact.artifacts.skill import SkillArtifact
21
+ from agent_env.artifact.artifacts.vm_image import VMImageArtifact
22
+
23
+ if TYPE_CHECKING:
24
+ from agent_env.config.runtime import Config
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _get_type(cls: type[Artifact]) -> Any:
30
+ return cls.model_fields["type"].default
31
+
32
+
33
+ ARTIFACT_REGISTRY: dict[str, type[Artifact]] = {
34
+ _get_type(CliArtifact): CliArtifact,
35
+ _get_type(FileArtifact): FileArtifact,
36
+ _get_type(FileArtifactUniverse): FileArtifactUniverse,
37
+ _get_type(DockerImageArtifact): DockerImageArtifact,
38
+ _get_type(EnvironmentArtifact): EnvironmentArtifact,
39
+ _get_type(EnvironmentUniverseArtifact): EnvironmentUniverseArtifact,
40
+ _get_type(SkillArtifact): SkillArtifact,
41
+ _get_type(VMImageArtifact): VMImageArtifact,
42
+ }
43
+
44
+
45
+
46
+
47
+ def _build_registry(source: Config | None = None) -> dict[str, type[Artifact]]:
48
+ """The built-in artifact types, then ``agent_env.artifacts`` plugins, then ``[artifacts] impls``
49
+ in ``source``."""
50
+ registry = dict(ARTIFACT_REGISTRY)
51
+ from_plugins = _registration.merge(registry, _registration.ARTIFACTS, _validate_plugin, source=source)
52
+ _merge_config_toml_artifacts(registry, source=source, from_plugins=from_plugins)
53
+ # After config, which may have replaced a class's own spelling and stranded its extra names.
54
+ for name in from_plugins:
55
+ if reason := _extra_name_problem(name, registry[name], registry, from_plugins.conflicts):
56
+ from_plugins.reject(name, _report.INVALID_PLUGIN, reason)
57
+ # This document's aliases, not the ambient Config's, or the two can disagree.
58
+ _check_type_aliases(registry, _load_type_aliases(source), from_plugins=from_plugins)
59
+ return registry
60
+
61
+
62
+ def _extra_name_problem(
63
+ name: str, cls: type[Artifact], registry: dict[str, type[Artifact]],
64
+ conflicts: Mapping[str, tuple[Any, ...]] | None = None,
65
+ ) -> str | None:
66
+ """Why a plugin's extra name (a legacy spelling) for ``cls`` would read nothing, if it would.
67
+
68
+ It only reads old documents: the class writes its own spelling, which must resolve to it,
69
+ and a stored document validates against the class's ``type`` field, which must accept it.
70
+ """
71
+ own = _get_type(cls)
72
+ if name == own:
73
+ return None
74
+ builtin = ARTIFACT_REGISTRY.get(own)
75
+ if builtin is not None and builtin is not cls:
76
+ return f"its class inherits the built-in type {own!r}; give it its own 'type' field default"
77
+ if conflicts and own in conflicts:
78
+ claimants = list(conflicts[own])
79
+ return f"its class writes type {own!r}, which is left out: {_registration.conflict_message(own, _registration.ARTIFACTS, claimants)}"
80
+ if registry.get(own) is not cls:
81
+ elsewhere = registry.get(own)
82
+ where = "nothing registers it" if elsewhere is None else f"it resolves to {elsewhere.__qualname__}"
83
+ return f"its class writes type {own!r}, but {where}; register that name to the class too"
84
+ try:
85
+ TypeAdapter(cls.model_fields["type"].annotation).validate_python(name)
86
+ except ValidationError:
87
+ return f"its 'type' field does not accept {name!r}, so no document stored under it can be read"
88
+ return None
89
+
90
+
91
+ def _validate_plugin(name: str, loaded: Any) -> type[Artifact]:
92
+ # One class may register under several names; _extra_name_problem checks the extra ones.
93
+ cls = _registration.require_subclass(loaded, Artifact)
94
+ if not isinstance(_get_type(cls), str):
95
+ raise TypeError(f"{cls.__qualname__} does not define its own 'type' field default")
96
+ if problem := _registration.unimplemented(cls, Artifact):
97
+ raise TypeError(problem)
98
+ return cls
99
+
100
+
101
+ def get_artifact_registry() -> dict[str, type[Artifact]]:
102
+ from agent_env.config import runtime
103
+
104
+ return runtime.get_config().artifact_registry()
105
+
106
+
107
+ def get_type_aliases() -> dict[str, str]:
108
+ """Legacy artifact `type` spellings mapped to the canonical one, from config.
109
+
110
+ Empty unless `[artifacts] type_aliases` declares any, and the core ships none: a
111
+ deployment that renamed an artifact type keeps reading the documents it already
112
+ wrote under the old spelling, without ever rewriting them. An installation that
113
+ never renamed a type has nothing to declare.
114
+
115
+ Registering a subclass under the old spelling expresses the same intent and stays
116
+ supported; the two are alternatives rather than layers, so declaring both for one
117
+ type is rejected.
118
+ """
119
+ from agent_env.config import runtime
120
+
121
+ return runtime.get_config().artifact_type_aliases()
122
+
123
+
124
+ def canonical_type(stored_type: str) -> str:
125
+ """The registry key `stored_type` resolves to — itself unless it is an alias."""
126
+ return get_type_aliases().get(stored_type, stored_type)
127
+
128
+
129
+ def equivalent_types(artifact_type: str) -> list[str]:
130
+ """Every stored spelling that resolves to the same artifact as `artifact_type`.
131
+
132
+ A `type` filter is evaluated server-side against the value as stored, so it has to
133
+ match documents written under either spelling — canonicalising on read happens far
134
+ too late to help it. Returns `[artifact_type]` unchanged when nothing aliases to it,
135
+ so an unaliased type still compiles to a scalar equality rather than a one-element
136
+ `$in`. The order is unspecified — `canonical_type` gives the representative spelling.
137
+ """
138
+ aliases = get_type_aliases()
139
+ canonical = aliases.get(artifact_type, artifact_type)
140
+ return sorted({artifact_type, canonical} | {a for a, c in aliases.items() if c == canonical})
141
+
142
+
143
+ def _load_type_aliases(source: Config | None = None) -> dict[str, str]:
144
+ from agent_env.config import runtime
145
+ from agent_env.config import ConfigError
146
+
147
+ # A nested walk, so a mis-shaped table raises the config layer's own message rather
148
+ # than a second hand-rolled one.
149
+ aliases = (source or runtime.get_config()).section("artifacts", "type_aliases")
150
+ for legacy, canonical in aliases.items():
151
+ if not isinstance(canonical, str):
152
+ raise ConfigError(
153
+ f"[artifacts] type_aliases {legacy!r} must map to a string, got "
154
+ f"{type(canonical).__name__}: {canonical!r}"
155
+ )
156
+ if canonical == legacy:
157
+ raise ConfigError(f"[artifacts] type_aliases {legacy!r} maps to itself")
158
+ if canonical in aliases:
159
+ raise ConfigError(
160
+ f"[artifacts] type_aliases {legacy!r} maps to {canonical!r}, which is itself "
161
+ f"an alias; chains are not resolved"
162
+ )
163
+ return dict(aliases)
164
+
165
+
166
+ def _check_type_aliases(
167
+ registry: dict[str, type[Artifact]],
168
+ aliases: dict[str, str],
169
+ *,
170
+ from_plugins: _registration.Registrations | None = None,
171
+ ) -> None:
172
+ from agent_env.config import ConfigError
173
+
174
+ from_plugins = from_plugins if from_plugins is not None else _registration.Registrations.empty()
175
+ for legacy, canonical in aliases.items():
176
+ if canonical not in registry:
177
+ if from_plugins.refuse(canonical, f"[artifacts] type_aliases {legacy!r}"):
178
+ continue
179
+ if from_plugins.failed(canonical):
180
+ # Like a config-only provider table for a failed plugin: skipped, not fatal, so
181
+ # one broken plugin does not take every artifact read down with it.
182
+ logger.warning(
183
+ "[artifacts] type_aliases %r maps to %r, whose plugin failed to load; skipped", legacy, canonical
184
+ )
185
+ continue
186
+ raise ConfigError(
187
+ f"[artifacts] type_aliases {legacy!r} maps to {canonical!r}, which no artifact "
188
+ f"registers; declare it under [artifacts].impls or correct the alias"
189
+ )
190
+ # Aliasing a type that is still registered is normal mid-rename, as long as both
191
+ # spellings mean the same class. Pointing it at a different class instead silently
192
+ # redirects every document that type owns.
193
+ if legacy in from_plugins.conflicts:
194
+ claimants = "; ".join(str(p) for p in from_plugins.conflicts[legacy])
195
+ raise ConfigError(
196
+ f"[artifacts] type_aliases {legacy!r} maps to {canonical!r}, but plugins {claimants} "
197
+ f"register {legacy!r}; uninstall them or drop the alias"
198
+ )
199
+ owner = registry.get(legacy)
200
+ if owner is not None and owner is not registry[canonical]:
201
+ if (plugin := from_plugins.plugin(legacy)) is not None:
202
+ raise ConfigError(
203
+ f"[artifacts] type_aliases {legacy!r} maps to {canonical!r}, but plugin {plugin} "
204
+ f"registers {legacy!r} to {owner.__name__}; uninstall the plugin or drop the alias"
205
+ )
206
+ raise ConfigError(
207
+ f"[artifacts] type_aliases {legacy!r} is registered to {owner.__name__} but "
208
+ f"aliased to {canonical!r} ({registry[canonical].__name__}); a type resolves "
209
+ f"either through its own registered class or through an alias, not both — "
210
+ f"drop it from impls, or drop the alias"
211
+ )
212
+
213
+
214
+ def _merge_config_toml_artifacts(
215
+ registry: dict[str, type[Artifact]],
216
+ *,
217
+ source: Config | None = None,
218
+ from_plugins: _registration.Registrations | None = None,
219
+ ) -> None:
220
+ from agent_env.config import runtime
221
+ from agent_env.config import ConfigError, load_impl
222
+
223
+ section = (source or runtime.get_config()).section("artifacts")
224
+ impls = section.get("impls", [])
225
+ if not isinstance(impls, list):
226
+ raise ConfigError(
227
+ f"[artifacts] impls must be a list of 'module:Class' strings, got {type(impls).__name__}"
228
+ )
229
+ from_plugins = from_plugins if from_plugins is not None else _registration.Registrations.empty()
230
+ for impl in impls:
231
+ if not isinstance(impl, str):
232
+ raise ConfigError(
233
+ f"[artifacts] impl must be a 'module:Class' string, got {type(impl).__name__}: {impl!r}"
234
+ )
235
+ cls = load_impl(impl, Artifact)
236
+ artifact_type = _get_type(cls)
237
+ if not isinstance(artifact_type, str):
238
+ raise ConfigError(
239
+ f"[artifacts] impl {impl!r} does not define its own 'type' "
240
+ f"(inherits the base Artifact default); set a concrete 'type' field default"
241
+ )
242
+ if problem := _registration.unimplemented(cls, Artifact):
243
+ raise ConfigError(f"[artifacts] impl {impl!r}: {problem}")
244
+ if from_plugins.refuse(artifact_type, f"[artifacts] impl {impl!r}"):
245
+ continue
246
+ if not from_plugins.release(artifact_type, "artifacts", impl, cls) and artifact_type in registry:
247
+ raise ConfigError(
248
+ f"[artifacts] impl {impl!r} type {artifact_type!r} is already registered "
249
+ f"(conflicts with a built-in or another custom artifact)"
250
+ )
251
+ registry[artifact_type] = cls