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.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- 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
|