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,775 @@
|
|
|
1
|
+
"""Load artifact into a deployed environment task step."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import posixpath
|
|
9
|
+
import shlex
|
|
10
|
+
import uuid
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable, ClassVar, Optional
|
|
13
|
+
from urllib.parse import unquote, urlparse
|
|
14
|
+
|
|
15
|
+
from agent_env.task_step.context import TaskStepContext
|
|
16
|
+
from agent_env.entity_refs import EntityRef
|
|
17
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
18
|
+
from agent_env.utils.paths import validate_relative_filename
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Where producer steps record what they made, keyed by their own step id. Resolving
|
|
23
|
+
# against these lets config name an artifact whose id is minted mid-run, without this
|
|
24
|
+
# step knowing which kind of step produced it. A step writes exactly one of these, so
|
|
25
|
+
# the order arbitrates a malformed context rather than ranking real producers.
|
|
26
|
+
_STEP_ARTIFACT_SOURCES: tuple[tuple[str, Callable[[dict], Optional[dict]]], ...] = (
|
|
27
|
+
("collected_artifacts", lambda entry: entry.get("file_artifact_universe")),
|
|
28
|
+
("env_snapshotted_universes", lambda entry: entry),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class _LoadInputs:
|
|
34
|
+
"""What a load_artifact step loads and where, after per-run overrides."""
|
|
35
|
+
|
|
36
|
+
artifacts: list[dict]
|
|
37
|
+
urls: list[str]
|
|
38
|
+
destination_path: Optional[str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LoadArtifactTaskStep(TaskStep):
|
|
42
|
+
type: ClassVar[str] = "load_artifact"
|
|
43
|
+
entity_refs = (
|
|
44
|
+
EntityRef.env("env_id"),
|
|
45
|
+
EntityRef.artifact("artifact_id", version_field="artifact_version"),
|
|
46
|
+
EntityRef.artifact("artifacts[].id", version_field="version"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
id: str,
|
|
52
|
+
version: Optional[int],
|
|
53
|
+
env_id: Optional[str] = None,
|
|
54
|
+
env_step_id: Optional[str] = None,
|
|
55
|
+
artifacts: Optional[list[dict]] = None,
|
|
56
|
+
artifact_id: Optional[str] = None,
|
|
57
|
+
artifact_version: Optional[int] = None,
|
|
58
|
+
urls: Optional[list[str]] = None,
|
|
59
|
+
collected_artifacts_step_id: Optional[str] = None,
|
|
60
|
+
artifact_from_step_id: Optional[str] = None,
|
|
61
|
+
destination_path: Optional[str] = None,
|
|
62
|
+
agent_name: Optional[str] = None,
|
|
63
|
+
sandbox_name: Optional[str] = None,
|
|
64
|
+
container_name: Optional[str] = None,
|
|
65
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
66
|
+
fail_task_on_error: bool = True,
|
|
67
|
+
snapshot_after_load: Optional[bool] = None,
|
|
68
|
+
):
|
|
69
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
70
|
+
if artifacts is not None and artifact_id is not None:
|
|
71
|
+
raise ValueError("Pass either `artifacts=[...]` or `artifact_id=...`, not both")
|
|
72
|
+
if artifacts is None and artifact_id is not None:
|
|
73
|
+
artifacts = [{"id": artifact_id, "version": artifact_version}]
|
|
74
|
+
if collected_artifacts_step_id and (artifacts or urls):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"`collected_artifacts_step_id` is mutually exclusive with `artifacts`/`artifact_id`/`urls`"
|
|
77
|
+
)
|
|
78
|
+
if artifact_from_step_id and (artifacts or urls or collected_artifacts_step_id):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"`artifact_from_step_id` is mutually exclusive with "
|
|
81
|
+
"`artifacts`/`artifact_id`/`urls`/`collected_artifacts_step_id`"
|
|
82
|
+
)
|
|
83
|
+
if not (artifacts or urls or collected_artifacts_step_id or artifact_from_step_id):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
"Must pass at least one of `artifacts=[...]`, `artifact_id=...`, `urls=[...]`, "
|
|
86
|
+
"`artifact_from_step_id=...`, or `collected_artifacts_step_id=...`"
|
|
87
|
+
)
|
|
88
|
+
if agent_name is not None and container_name is not None:
|
|
89
|
+
raise ValueError("Pass either `agent_name` or `container_name`, not both")
|
|
90
|
+
if env_id is None and agent_name is None and container_name is None and sandbox_name is None:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"Must pass at least one of `env_id`, `agent_name`, `sandbox_name`, or "
|
|
93
|
+
"`container_name`+`sandbox_name`"
|
|
94
|
+
)
|
|
95
|
+
if container_name is not None and sandbox_name is None:
|
|
96
|
+
raise ValueError("`container_name` requires `sandbox_name`")
|
|
97
|
+
if urls and container_name is None and agent_name is None and sandbox_name is None:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"`urls` requires `agent_name`, `sandbox_name`, or `container_name`+`sandbox_name`"
|
|
100
|
+
)
|
|
101
|
+
if collected_artifacts_step_id and agent_name is None and container_name is None and sandbox_name is None:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"`collected_artifacts_step_id` requires `agent_name`, `sandbox_name`, or "
|
|
104
|
+
"`container_name`+`sandbox_name`"
|
|
105
|
+
)
|
|
106
|
+
self.env_id = env_id
|
|
107
|
+
self.env_step_id = env_step_id
|
|
108
|
+
self.artifacts = artifacts or []
|
|
109
|
+
self.urls = list(urls or [])
|
|
110
|
+
# Superseded by the producer-agnostic `artifact_from_step_id`, but kept
|
|
111
|
+
# readable/writable: it is persisted on existing task-step documents.
|
|
112
|
+
self.collected_artifacts_step_id = collected_artifacts_step_id
|
|
113
|
+
self.artifact_from_step_id = artifact_from_step_id
|
|
114
|
+
self.destination_path = destination_path
|
|
115
|
+
self.agent_name = agent_name
|
|
116
|
+
self.sandbox_name = sandbox_name
|
|
117
|
+
self.container_name = container_name
|
|
118
|
+
# None = defer to the AGENT_ENV_SNAPSHOT_AFTER_LOAD flag on the worker, so a task
|
|
119
|
+
# doc doesn't hard-code a cost decision an operator may want to flip.
|
|
120
|
+
self.snapshot_after_load = snapshot_after_load
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict:
|
|
123
|
+
base = super().to_dict()
|
|
124
|
+
base["env_id"] = self.env_id
|
|
125
|
+
base["env_step_id"] = self.env_step_id
|
|
126
|
+
base["artifacts"] = self.artifacts
|
|
127
|
+
base["urls"] = self.urls
|
|
128
|
+
base["collected_artifacts_step_id"] = self.collected_artifacts_step_id
|
|
129
|
+
base["artifact_from_step_id"] = self.artifact_from_step_id
|
|
130
|
+
base["destination_path"] = self.destination_path
|
|
131
|
+
base["agent_name"] = self.agent_name
|
|
132
|
+
base["sandbox_name"] = self.sandbox_name
|
|
133
|
+
base["container_name"] = self.container_name
|
|
134
|
+
base["snapshot_after_load"] = self.snapshot_after_load
|
|
135
|
+
return base
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_dict(cls, data: dict) -> LoadArtifactTaskStep:
|
|
139
|
+
if "artifacts" in data:
|
|
140
|
+
artifacts = data["artifacts"]
|
|
141
|
+
elif "artifact_id" in data:
|
|
142
|
+
artifacts = [{"id": data["artifact_id"], "version": data.get("artifact_version")}]
|
|
143
|
+
else:
|
|
144
|
+
artifacts = None
|
|
145
|
+
return cls(
|
|
146
|
+
**cls._base_from_dict(data),
|
|
147
|
+
env_id=data.get("env_id"),
|
|
148
|
+
env_step_id=data.get("env_step_id"),
|
|
149
|
+
artifacts=artifacts,
|
|
150
|
+
urls=data.get("urls"),
|
|
151
|
+
collected_artifacts_step_id=data.get("collected_artifacts_step_id"),
|
|
152
|
+
artifact_from_step_id=data.get("artifact_from_step_id"),
|
|
153
|
+
destination_path=data.get("destination_path"),
|
|
154
|
+
agent_name=data.get("agent_name"),
|
|
155
|
+
sandbox_name=data.get("sandbox_name"),
|
|
156
|
+
container_name=data.get("container_name"),
|
|
157
|
+
snapshot_after_load=data.get("snapshot_after_load"),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _record_universe_load(context: TaskStepContext, artifact, load_result) -> None:
|
|
162
|
+
"""Record which load path ran, so a slow run is explicable after the fact.
|
|
163
|
+
|
|
164
|
+
"This took 40 minutes" and "this took 90 seconds" are the same step with the same
|
|
165
|
+
inputs; the only difference is whether a snapshot was hit. Without this the
|
|
166
|
+
distinction exists solely in worker logs, which are gone by the time anyone asks.
|
|
167
|
+
"""
|
|
168
|
+
if load_result is None:
|
|
169
|
+
return
|
|
170
|
+
entry = {
|
|
171
|
+
"id": artifact.id,
|
|
172
|
+
"version": artifact.version,
|
|
173
|
+
"restored_from_snapshot": getattr(load_result, "restored_from_snapshot", False),
|
|
174
|
+
}
|
|
175
|
+
for key in ("snapshot_db_image_artifact_id", "snapshot_baked", "snapshot_bake_error"):
|
|
176
|
+
value = getattr(load_result, key, None)
|
|
177
|
+
if value is not None:
|
|
178
|
+
entry[key] = value
|
|
179
|
+
context.metadata.setdefault("loaded_environment_universes", []).append(entry)
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _resolve_step_artifact(context: TaskStepContext, step_id: str) -> Optional[dict]:
|
|
183
|
+
"""The ``{id, version}`` a prior step recorded under ``step_id``.
|
|
184
|
+
|
|
185
|
+
``None`` when that step ran but produced nothing; raises when nothing was
|
|
186
|
+
recorded at all, since a wrong id or a producer that never ran is
|
|
187
|
+
misconfiguration rather than an empty result.
|
|
188
|
+
"""
|
|
189
|
+
for key, extract in _STEP_ARTIFACT_SOURCES:
|
|
190
|
+
entry = (context.metadata.get(key) or {}).get(step_id)
|
|
191
|
+
if entry is None:
|
|
192
|
+
continue
|
|
193
|
+
ref = extract(entry)
|
|
194
|
+
return {"id": ref["id"], "version": ref.get("version")} if ref else None
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
f"No artifact recorded by step '{step_id}'. Ensure a step that produces "
|
|
197
|
+
f"one (e.g. `collect_artifacts`, `snapshot_env`) ran with that id before "
|
|
198
|
+
f"this load step."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _deploy_step_of(deployed_env) -> Optional[str]:
|
|
203
|
+
return (deployed_env.metadata or {}).get("deploy_step_id")
|
|
204
|
+
|
|
205
|
+
def _deployed_env(self, context: TaskStepContext):
|
|
206
|
+
"""The deployment of `env_id` this step loads into, or None if it isn't deployed."""
|
|
207
|
+
matches = [d for d in context.deployed_envs if d.env_id == self.env_id]
|
|
208
|
+
if self.env_step_id is not None:
|
|
209
|
+
# A filter, not a hint: an unmatched id is an error, never a substitute.
|
|
210
|
+
named = [d for d in matches if self._deploy_step_of(d) == self.env_step_id]
|
|
211
|
+
if matches and not named:
|
|
212
|
+
raise RuntimeError(
|
|
213
|
+
f"load_artifact '{self.id}': env_step_id '{self.env_step_id}' names no deployment of "
|
|
214
|
+
f"env '{self.env_id}' (deployed by: {[self._deploy_step_of(d) for d in matches]})"
|
|
215
|
+
)
|
|
216
|
+
matches = named
|
|
217
|
+
if len(matches) > 1:
|
|
218
|
+
raise RuntimeError(
|
|
219
|
+
f"load_artifact '{self.id}': {len(matches)} deployments of env '{self.env_id}' "
|
|
220
|
+
f"(deployed by: {[self._deploy_step_of(d) for d in matches]}). Set env_step_id "
|
|
221
|
+
"to the deploy_env step this load should target."
|
|
222
|
+
)
|
|
223
|
+
return matches[0] if matches else None
|
|
224
|
+
|
|
225
|
+
def _resolve_inputs(self, context: TaskStepContext) -> _LoadInputs:
|
|
226
|
+
"""Resolve ``(artifacts, urls, destination_path)``, honoring per-run
|
|
227
|
+
``step_overrides``. An override wins over the stored value; an explicit
|
|
228
|
+
artifact override also wins over ``artifact_from_step_id`` /
|
|
229
|
+
``collected_artifacts_step_id`` wiring.
|
|
230
|
+
Topology (env_id/agent_name/container_name) is not overridable here.
|
|
231
|
+
"""
|
|
232
|
+
overrides = self.step_param_overrides(context)
|
|
233
|
+
|
|
234
|
+
if "artifacts" in overrides:
|
|
235
|
+
resolved_artifacts = list(overrides.get("artifacts") or [])
|
|
236
|
+
elif "artifact_id" in overrides:
|
|
237
|
+
if not overrides["artifact_id"]:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"load '{self.id}': step_overrides 'artifact_id' must be a non-empty string, "
|
|
240
|
+
f"got {overrides['artifact_id']!r}"
|
|
241
|
+
)
|
|
242
|
+
resolved_artifacts = [
|
|
243
|
+
{"id": overrides["artifact_id"], "version": overrides.get("artifact_version")}
|
|
244
|
+
]
|
|
245
|
+
else:
|
|
246
|
+
resolved_artifacts = list(self.artifacts)
|
|
247
|
+
step_id = self.artifact_from_step_id or self.collected_artifacts_step_id
|
|
248
|
+
if step_id:
|
|
249
|
+
ref = self._resolve_step_artifact(context, step_id)
|
|
250
|
+
# Produced nothing → skip the load and let a downstream verifier
|
|
251
|
+
# score the empty result, rather than failing the task here.
|
|
252
|
+
if ref is None:
|
|
253
|
+
logger.warning("step %r produced no artifacts — skipping load", step_id)
|
|
254
|
+
resolved_artifacts = []
|
|
255
|
+
else:
|
|
256
|
+
resolved_artifacts = [ref]
|
|
257
|
+
|
|
258
|
+
# `or []` so a `{"urls": null}` override cleanly clears rather than crashing on list(None).
|
|
259
|
+
urls = list(overrides["urls"] or []) if "urls" in overrides else list(self.urls)
|
|
260
|
+
destination_path = overrides.get("destination_path", self.destination_path)
|
|
261
|
+
|
|
262
|
+
if overrides:
|
|
263
|
+
logger.info("load '%s': applying step_overrides for %s", self.id, sorted(overrides))
|
|
264
|
+
return _LoadInputs(artifacts=resolved_artifacts, urls=urls, destination_path=destination_path)
|
|
265
|
+
|
|
266
|
+
async def _load_url_onto_vm(self, sandbox, url: str, destination_path: str) -> None:
|
|
267
|
+
"""Host counterpart of ``sandbox.load_s3_file``; ``write_file_from_url`` targets a container instead."""
|
|
268
|
+
from agent_env.providers.sandbox_providers.sandbox import CURL_RETRY_FLAGS
|
|
269
|
+
|
|
270
|
+
parent = posixpath.dirname(destination_path)
|
|
271
|
+
if parent:
|
|
272
|
+
await sandbox.exec_script(f"mkdir -p {shlex.quote(parent)}")
|
|
273
|
+
await sandbox.exec_script(
|
|
274
|
+
f"curl -fsSL {CURL_RETRY_FLAGS} {shlex.quote(url)} -o {shlex.quote(destination_path)}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
278
|
+
from agent_env.a2a_agent import A2AAgent
|
|
279
|
+
from agent_env.a2a_agent.store import get_a2a_agent_instance_store
|
|
280
|
+
from agent_env.artifact import (
|
|
281
|
+
Artifact,
|
|
282
|
+
CliArtifact,
|
|
283
|
+
FileArtifact,
|
|
284
|
+
FileArtifactUniverse,
|
|
285
|
+
EnvironmentArtifact,
|
|
286
|
+
EnvironmentUniverseArtifact,
|
|
287
|
+
)
|
|
288
|
+
from agent_env.env.env import Env, require_gateway_url
|
|
289
|
+
from agent_env.providers.sandbox_providers.sandbox import VmSandbox
|
|
290
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import (
|
|
291
|
+
build_sandbox_provider,
|
|
292
|
+
get_agent_sandbox_provider,
|
|
293
|
+
get_sandbox_provider,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
deployed = None
|
|
297
|
+
if self.env_id is not None:
|
|
298
|
+
deployed = self._deployed_env(context)
|
|
299
|
+
if deployed is None:
|
|
300
|
+
raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
|
|
301
|
+
|
|
302
|
+
inputs = self._resolve_inputs(context)
|
|
303
|
+
resolved_artifacts = inputs.artifacts
|
|
304
|
+
urls = inputs.urls
|
|
305
|
+
destination_path = inputs.destination_path
|
|
306
|
+
|
|
307
|
+
# With container_name it hosts that container; alone it is the target itself.
|
|
308
|
+
target_sandbox = None
|
|
309
|
+
if self.sandbox_name is not None:
|
|
310
|
+
if self.container_name is not None:
|
|
311
|
+
containers = context.metadata.get("deployed_docker_containers", [])
|
|
312
|
+
container_entry = next(
|
|
313
|
+
(
|
|
314
|
+
c for c in containers
|
|
315
|
+
if c.get("container_name") == self.container_name
|
|
316
|
+
and c.get("sandbox_name") == self.sandbox_name
|
|
317
|
+
),
|
|
318
|
+
None,
|
|
319
|
+
)
|
|
320
|
+
if container_entry is None:
|
|
321
|
+
raise RuntimeError(
|
|
322
|
+
f"Container '{self.container_name}' not found on sandbox '{self.sandbox_name}' in "
|
|
323
|
+
f"context.metadata['deployed_docker_containers']"
|
|
324
|
+
)
|
|
325
|
+
ds = next((s for s in context.deployed_sandboxes if s.sandbox_name == self.sandbox_name), None)
|
|
326
|
+
if ds is None:
|
|
327
|
+
raise RuntimeError(f"Sandbox '{self.sandbox_name}' not found in context.deployed_sandboxes")
|
|
328
|
+
provider = (
|
|
329
|
+
build_sandbox_provider(ds.sandbox_type) if ds.sandbox_type else get_sandbox_provider()
|
|
330
|
+
)
|
|
331
|
+
target_sandbox = await provider.get_sandbox(ds.sandbox_id)
|
|
332
|
+
|
|
333
|
+
env = None
|
|
334
|
+
for ref in resolved_artifacts:
|
|
335
|
+
artifact = Artifact.get(ref["id"], ref.get("version"))
|
|
336
|
+
# Split on the target, not the type: a service universe aimed at an
|
|
337
|
+
# agent/container is a grading load (stage the frozen exports as files),
|
|
338
|
+
# aimed at an env it still means restore into the live MCP services.
|
|
339
|
+
stage_as_files = isinstance(artifact, (FileArtifactUniverse, FileArtifact)) or (
|
|
340
|
+
isinstance(artifact, EnvironmentUniverseArtifact)
|
|
341
|
+
and deployed is None
|
|
342
|
+
and (
|
|
343
|
+
self.agent_name is not None
|
|
344
|
+
or self.container_name is not None
|
|
345
|
+
or self.sandbox_name is not None
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
if stage_as_files:
|
|
349
|
+
if self.sandbox_name is not None:
|
|
350
|
+
destination = (destination_path or "/loaded").rstrip("/") or "/"
|
|
351
|
+
if self.container_name is not None:
|
|
352
|
+
files = await _load_universe_into_container(
|
|
353
|
+
target_sandbox, self.container_name, artifact, destination,
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
files = await _load_universe_onto_vm(target_sandbox, artifact, destination)
|
|
357
|
+
context.metadata.setdefault("loaded_file_artifact_universes", []).append({
|
|
358
|
+
"id": artifact.id,
|
|
359
|
+
"version": artifact.version,
|
|
360
|
+
"artifact_type": artifact.type,
|
|
361
|
+
"env_id": None,
|
|
362
|
+
"agent_name": None,
|
|
363
|
+
"sandbox_name": self.sandbox_name,
|
|
364
|
+
"container_name": self.container_name,
|
|
365
|
+
"destination_path": destination,
|
|
366
|
+
"files": files,
|
|
367
|
+
})
|
|
368
|
+
continue
|
|
369
|
+
if (self.env_id is None) == (self.agent_name is None):
|
|
370
|
+
raise RuntimeError(
|
|
371
|
+
f"{artifact.type} '{artifact.id}' requires exactly one of `env_id` or `agent_name`"
|
|
372
|
+
)
|
|
373
|
+
if self.env_id is not None:
|
|
374
|
+
# Only a FileArtifactUniverse or a FileArtifact reaches here — `stage_as_files`
|
|
375
|
+
# excludes a service universe whenever an env is targeted.
|
|
376
|
+
if env is None:
|
|
377
|
+
env_obj = Env.get(deployed.env_id, deployed.env_version)
|
|
378
|
+
env = await type(env_obj).from_deployed_env(deployed)
|
|
379
|
+
result = await env.load_file_artifact_universe(artifact, destination_path=destination_path)
|
|
380
|
+
context.metadata.setdefault("loaded_file_artifact_universes", []).append({
|
|
381
|
+
"id": artifact.id,
|
|
382
|
+
"version": artifact.version,
|
|
383
|
+
"artifact_type": artifact.type,
|
|
384
|
+
"env_id": deployed.env_id,
|
|
385
|
+
"agent_name": None,
|
|
386
|
+
"destination_path": result.destination_path,
|
|
387
|
+
"files": result.files,
|
|
388
|
+
})
|
|
389
|
+
else:
|
|
390
|
+
agent = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
|
|
391
|
+
if agent is None or not agent.instance_id:
|
|
392
|
+
raise RuntimeError(f"Agent '{self.agent_name}' not found in context.deployed_agents (or missing instance_id)")
|
|
393
|
+
deployed_agent = get_a2a_agent_instance_store().get(agent.instance_id)
|
|
394
|
+
destination = (destination_path or "/tmp/file_artifacts").rstrip("/") or "/"
|
|
395
|
+
files = await A2AAgent.load_file_artifact_universe(deployed_agent, artifact, destination)
|
|
396
|
+
context.metadata.setdefault("loaded_file_artifact_universes", []).append({
|
|
397
|
+
"id": artifact.id,
|
|
398
|
+
"version": artifact.version,
|
|
399
|
+
"artifact_type": artifact.type,
|
|
400
|
+
"env_id": None,
|
|
401
|
+
"agent_name": self.agent_name,
|
|
402
|
+
"destination_path": destination,
|
|
403
|
+
"files": files,
|
|
404
|
+
})
|
|
405
|
+
elif isinstance(artifact, (EnvironmentArtifact, EnvironmentUniverseArtifact)):
|
|
406
|
+
if (
|
|
407
|
+
deployed is None
|
|
408
|
+
and isinstance(artifact, EnvironmentArtifact)
|
|
409
|
+
and (
|
|
410
|
+
self.agent_name is not None
|
|
411
|
+
or self.container_name is not None
|
|
412
|
+
or self.sandbox_name is not None
|
|
413
|
+
)
|
|
414
|
+
):
|
|
415
|
+
# Stage the service payload (filesystem-style data.json with
|
|
416
|
+
# `files[]`, or a `data.json` + `root/` doc-carrier zip) as a
|
|
417
|
+
# plain file tree inside the agent/container — no MCP server
|
|
418
|
+
# involved.
|
|
419
|
+
if self.sandbox_name is not None:
|
|
420
|
+
sandbox = target_sandbox
|
|
421
|
+
# None targets the VM host itself.
|
|
422
|
+
container = self.container_name
|
|
423
|
+
else:
|
|
424
|
+
agent = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
|
|
425
|
+
if agent is None or not agent.sandbox_id:
|
|
426
|
+
raise RuntimeError(
|
|
427
|
+
f"Agent '{self.agent_name}' not found in context.deployed_agents (or missing sandbox_id)"
|
|
428
|
+
)
|
|
429
|
+
provider = (
|
|
430
|
+
build_sandbox_provider(agent.sandbox_type)
|
|
431
|
+
if agent.sandbox_type
|
|
432
|
+
else get_agent_sandbox_provider()
|
|
433
|
+
)
|
|
434
|
+
sandbox = await provider.get_sandbox(agent.sandbox_id)
|
|
435
|
+
container = sandbox.container_name if isinstance(sandbox, VmSandbox) else None
|
|
436
|
+
destination = (destination_path or "/app/files").rstrip("/") or "/"
|
|
437
|
+
files = await _stage_environment_payload_into_container(
|
|
438
|
+
sandbox, container, artifact, destination,
|
|
439
|
+
)
|
|
440
|
+
context.metadata.setdefault("loaded_environment_artifacts", []).append({
|
|
441
|
+
"id": artifact.id,
|
|
442
|
+
"version": artifact.version,
|
|
443
|
+
"service_name": artifact.environment_name,
|
|
444
|
+
"environment_name": artifact.environment_name,
|
|
445
|
+
"agent_name": self.agent_name,
|
|
446
|
+
"sandbox_name": self.sandbox_name,
|
|
447
|
+
"container_name": self.container_name,
|
|
448
|
+
"destination_path": destination,
|
|
449
|
+
"file_count": len(files),
|
|
450
|
+
# Cap the recorded list — TaskStepContext is persisted and
|
|
451
|
+
# can be shipped to an external runner; a huge corpus must
|
|
452
|
+
# not bloat the payload.
|
|
453
|
+
"files": files[:_ENVIRONMENT_PAYLOAD_MAX_RECORDED_FILES],
|
|
454
|
+
})
|
|
455
|
+
continue
|
|
456
|
+
if deployed is None:
|
|
457
|
+
raise RuntimeError(f"`env_id` is required when loading a {artifact.type}")
|
|
458
|
+
if env is None:
|
|
459
|
+
env_obj = Env.get(deployed.env_id, deployed.env_version)
|
|
460
|
+
env = await type(env_obj).from_deployed_env(deployed)
|
|
461
|
+
if isinstance(artifact, EnvironmentArtifact):
|
|
462
|
+
await env.load_environment_artifact(artifact)
|
|
463
|
+
else:
|
|
464
|
+
from agent_env.env.envs.multi_env import MultiEnv
|
|
465
|
+
|
|
466
|
+
# Only MultiEnv has a servicedb to snapshot; the other env types
|
|
467
|
+
# don't accept the kwarg, so don't invent one for them.
|
|
468
|
+
if isinstance(env, MultiEnv):
|
|
469
|
+
load_result = await env.load_environment_universe_artifact(
|
|
470
|
+
artifact, snapshot_after_load=self.snapshot_after_load
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
load_result = await env.load_environment_universe_artifact(artifact)
|
|
474
|
+
self._record_universe_load(context, artifact, load_result)
|
|
475
|
+
elif isinstance(artifact, CliArtifact):
|
|
476
|
+
if not self.agent_name:
|
|
477
|
+
raise RuntimeError("agent_name is required when loading a CliArtifact")
|
|
478
|
+
if deployed is None:
|
|
479
|
+
raise RuntimeError("env_id is required when loading a CliArtifact (for gateway_url)")
|
|
480
|
+
agent = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
|
|
481
|
+
if agent is None or not agent.instance_id:
|
|
482
|
+
raise RuntimeError(f"Agent '{self.agent_name}' not found in context.deployed_agents (or missing instance_id)")
|
|
483
|
+
deployed_agent = get_a2a_agent_instance_store().get(agent.instance_id)
|
|
484
|
+
gateway_url = require_gateway_url(deployed, "Installing a CliArtifact")
|
|
485
|
+
install_path = await A2AAgent.install_cli(deployed_agent, artifact, gateway_url)
|
|
486
|
+
agent_clis = context.metadata.setdefault("installed_clis", {}).setdefault(self.agent_name, {})
|
|
487
|
+
agent_clis[artifact.id] = {
|
|
488
|
+
"install_path": install_path,
|
|
489
|
+
"command_name": artifact.command_name,
|
|
490
|
+
}
|
|
491
|
+
else:
|
|
492
|
+
raise ValueError(f"Unsupported artifact type for loading: '{artifact.type}'")
|
|
493
|
+
|
|
494
|
+
if urls:
|
|
495
|
+
if self.sandbox_name is not None:
|
|
496
|
+
sandbox = target_sandbox
|
|
497
|
+
else:
|
|
498
|
+
agent = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
|
|
499
|
+
if agent is None or not agent.sandbox_id:
|
|
500
|
+
raise RuntimeError(f"Agent '{self.agent_name}' not found in context.deployed_agents (or missing sandbox_id)")
|
|
501
|
+
provider = (
|
|
502
|
+
build_sandbox_provider(agent.sandbox_type)
|
|
503
|
+
if agent.sandbox_type
|
|
504
|
+
else get_agent_sandbox_provider()
|
|
505
|
+
)
|
|
506
|
+
sandbox = await provider.get_sandbox(agent.sandbox_id)
|
|
507
|
+
destination = (destination_path or "/tmp/file_artifacts").rstrip("/") or "/"
|
|
508
|
+
|
|
509
|
+
files: list[str] = []
|
|
510
|
+
seen: dict[str, int] = {}
|
|
511
|
+
for url in urls:
|
|
512
|
+
base = unquote(os.path.basename(urlparse(url).path)) or "downloaded"
|
|
513
|
+
count = seen.get(base, 0)
|
|
514
|
+
seen[base] = count + 1
|
|
515
|
+
if count == 0:
|
|
516
|
+
files.append(base)
|
|
517
|
+
else:
|
|
518
|
+
stem, ext = os.path.splitext(base)
|
|
519
|
+
files.append(f"{stem}-{count}{ext}")
|
|
520
|
+
|
|
521
|
+
semaphore = asyncio.Semaphore(8)
|
|
522
|
+
|
|
523
|
+
async def _load_one(url: str, filename: str) -> None:
|
|
524
|
+
async with semaphore:
|
|
525
|
+
dest = f"{destination}/{filename}"
|
|
526
|
+
if self.sandbox_name is not None and self.container_name is None:
|
|
527
|
+
await self._load_url_onto_vm(sandbox, url, dest)
|
|
528
|
+
else:
|
|
529
|
+
await sandbox.write_file_from_url(url, dest)
|
|
530
|
+
target_desc = (
|
|
531
|
+
f"VM sandbox '{self.sandbox_name}'"
|
|
532
|
+
if self.sandbox_name is not None and self.container_name is None
|
|
533
|
+
else "agent"
|
|
534
|
+
)
|
|
535
|
+
logger.info(f"Loaded URL into {target_desc}: {url} -> {dest}")
|
|
536
|
+
|
|
537
|
+
await asyncio.gather(*(_load_one(u, f) for u, f in zip(urls, files)))
|
|
538
|
+
context.metadata.setdefault("loaded_urls", []).append({
|
|
539
|
+
"step_id": self.id,
|
|
540
|
+
"agent_name": None if self.sandbox_name is not None else self.agent_name,
|
|
541
|
+
"sandbox_name": self.sandbox_name,
|
|
542
|
+
"destination_path": destination,
|
|
543
|
+
"files": files,
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
return context
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# Cap on how many staged paths get recorded in TaskStepContext metadata
|
|
550
|
+
# (persisted, and shipped to external runners) — `file_count` always carries
|
|
551
|
+
# the real total.
|
|
552
|
+
_ENVIRONMENT_PAYLOAD_MAX_RECORDED_FILES = 200
|
|
553
|
+
|
|
554
|
+
# Expands a service payload into a plain file tree using only the Python
|
|
555
|
+
# stdlib, so it can run on the sandbox VM or inside the agent container
|
|
556
|
+
# without any agent-env install. Mirrors the synthetic filesystem server's
|
|
557
|
+
# load contract (BaseService._reset_from_zip_bundle / load_from_json):
|
|
558
|
+
# • zip bundle: `root/` members are real files (streamed out first, zip-slip
|
|
559
|
+
# guarded); an optional top-level `data.json` provides `files[]` entries
|
|
560
|
+
# that are skipped when the path already exists (root/ takes precedence).
|
|
561
|
+
# • bare data.json: `files[]` entries with `path`, `content`, and optional
|
|
562
|
+
# `encoding: "base64"` for binary content.
|
|
563
|
+
# argv: [<payload path>, <destination dir>]; prints one staged relative path
|
|
564
|
+
# per line.
|
|
565
|
+
_ENVIRONMENT_PAYLOAD_EXPAND_SCRIPT = """\
|
|
566
|
+
import base64, json, os, shutil, sys, zipfile
|
|
567
|
+
|
|
568
|
+
payload, dest = sys.argv[1], sys.argv[2]
|
|
569
|
+
staged = []
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _target(rel):
|
|
573
|
+
rel = rel.replace("\\\\", "/")
|
|
574
|
+
parts = [p for p in rel.split("/") if p not in ("", ".")]
|
|
575
|
+
if not parts or ".." in parts:
|
|
576
|
+
raise SystemExit("unsafe or empty path in payload: %r" % rel)
|
|
577
|
+
path = os.path.join(dest, *parts)
|
|
578
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
579
|
+
return path, "/".join(parts)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
os.makedirs(dest, exist_ok=True)
|
|
583
|
+
entries = []
|
|
584
|
+
if zipfile.is_zipfile(payload):
|
|
585
|
+
with zipfile.ZipFile(payload) as zf:
|
|
586
|
+
data_json = None
|
|
587
|
+
# root/ members land first so data.json entries defer to real files —
|
|
588
|
+
# same precedence as the filesystem server's zip-bundle reset.
|
|
589
|
+
for info in zf.infolist():
|
|
590
|
+
if info.is_dir():
|
|
591
|
+
continue
|
|
592
|
+
if info.filename == "data.json":
|
|
593
|
+
data_json = info
|
|
594
|
+
continue
|
|
595
|
+
if info.filename.startswith("root/") and info.filename[len("root/"):]:
|
|
596
|
+
path, rel = _target(info.filename[len("root/"):])
|
|
597
|
+
with zf.open(info) as src, open(path, "wb") as out:
|
|
598
|
+
shutil.copyfileobj(src, out)
|
|
599
|
+
staged.append(rel)
|
|
600
|
+
if data_json is not None:
|
|
601
|
+
with zf.open(data_json) as f:
|
|
602
|
+
entries = json.load(f).get("files", []) or []
|
|
603
|
+
else:
|
|
604
|
+
with open(payload, "r", encoding="utf-8") as f:
|
|
605
|
+
entries = json.load(f).get("files", []) or []
|
|
606
|
+
|
|
607
|
+
for entry in entries:
|
|
608
|
+
rel = entry.get("path")
|
|
609
|
+
if not rel:
|
|
610
|
+
continue
|
|
611
|
+
path, rel_norm = _target(rel)
|
|
612
|
+
if os.path.exists(path):
|
|
613
|
+
continue
|
|
614
|
+
content = entry.get("content", "")
|
|
615
|
+
if entry.get("encoding") == "base64":
|
|
616
|
+
with open(path, "wb") as f:
|
|
617
|
+
f.write(base64.b64decode(content))
|
|
618
|
+
else:
|
|
619
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
620
|
+
f.write(content)
|
|
621
|
+
staged.append(rel_norm)
|
|
622
|
+
|
|
623
|
+
for rel in staged:
|
|
624
|
+
print(rel)
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
async def _stage_environment_payload_into_container(
|
|
629
|
+
sandbox, container_name: Optional[str], environment_artifact, destination: str
|
|
630
|
+
) -> list[str]:
|
|
631
|
+
"""Expand an EnvironmentArtifact's payload into a file tree at ``destination`` — in the container, or on the VM host when ``container_name`` is None."""
|
|
632
|
+
from agent_env.providers.sandbox_providers.sandbox import VmSandbox
|
|
633
|
+
|
|
634
|
+
file_artifact = environment_artifact.get_file_artifact()
|
|
635
|
+
token = uuid.uuid4().hex[:8]
|
|
636
|
+
safe_name = file_artifact.filename.replace("/", "_")
|
|
637
|
+
|
|
638
|
+
if isinstance(sandbox, VmSandbox):
|
|
639
|
+
vm_payload = f"/tmp/_svc_{token}_{safe_name}"
|
|
640
|
+
vm_stage = f"/tmp/_svc_stage_{token}"
|
|
641
|
+
await sandbox.load_s3_file(file_artifact.object_url, vm_payload)
|
|
642
|
+
try:
|
|
643
|
+
# exec_script raises on a non-zero exit, but only with the raw
|
|
644
|
+
# script output — re-raise with the artifact context so a failed
|
|
645
|
+
# expansion (corrupt payload, zip-slip refusal) is diagnosable,
|
|
646
|
+
# matching the container-mode branch below.
|
|
647
|
+
try:
|
|
648
|
+
out = await sandbox.exec_script(
|
|
649
|
+
f"python3 - {shlex.quote(vm_payload)} {shlex.quote(vm_stage)} <<'AGENTENV_EXPAND_PY'\n"
|
|
650
|
+
f"{_ENVIRONMENT_PAYLOAD_EXPAND_SCRIPT}\nAGENTENV_EXPAND_PY"
|
|
651
|
+
)
|
|
652
|
+
except RuntimeError as e:
|
|
653
|
+
raise RuntimeError(
|
|
654
|
+
f"Service payload expansion failed for "
|
|
655
|
+
f"'{environment_artifact.id}' v{environment_artifact.version}: {e}"
|
|
656
|
+
) from e
|
|
657
|
+
if container_name is None:
|
|
658
|
+
# Targeting the VM host: the tree is already on it, so it only moves.
|
|
659
|
+
await sandbox.exec_script(
|
|
660
|
+
f"mkdir -p {shlex.quote(destination)} && "
|
|
661
|
+
f"cp -a {shlex.quote(vm_stage)}/. {shlex.quote(destination)}/"
|
|
662
|
+
)
|
|
663
|
+
else:
|
|
664
|
+
await sandbox.exec_script(
|
|
665
|
+
f"sudo docker exec -u 0 {shlex.quote(container_name)} mkdir -p {shlex.quote(destination)}"
|
|
666
|
+
)
|
|
667
|
+
await sandbox.exec_script(
|
|
668
|
+
f"sudo docker cp {shlex.quote(vm_stage)}/. "
|
|
669
|
+
f"{shlex.quote(container_name)}:{shlex.quote(destination)}"
|
|
670
|
+
)
|
|
671
|
+
finally:
|
|
672
|
+
try:
|
|
673
|
+
await sandbox.exec_script(f"rm -rf {shlex.quote(vm_payload)} {shlex.quote(vm_stage)}")
|
|
674
|
+
except Exception as e:
|
|
675
|
+
logger.warning(f"Best-effort cleanup of service payload staging failed (ignored): {e}")
|
|
676
|
+
else:
|
|
677
|
+
# Container-mode sandbox: the sandbox IS the agent container — expand
|
|
678
|
+
# in place at the destination.
|
|
679
|
+
payload_tmp = f"/tmp/_svc_{token}_{safe_name}"
|
|
680
|
+
await sandbox.write_file_from_s3(file_artifact.object_url, payload_tmp)
|
|
681
|
+
try:
|
|
682
|
+
exit_code, out, err = await sandbox.exec_with_output(
|
|
683
|
+
"python3", "-c", _ENVIRONMENT_PAYLOAD_EXPAND_SCRIPT, payload_tmp, destination
|
|
684
|
+
)
|
|
685
|
+
if exit_code != 0:
|
|
686
|
+
raise RuntimeError(
|
|
687
|
+
f"Service payload expansion failed (exit {exit_code}) for "
|
|
688
|
+
f"'{environment_artifact.id}' v{environment_artifact.version}: {err[-1500:]}"
|
|
689
|
+
)
|
|
690
|
+
finally:
|
|
691
|
+
try:
|
|
692
|
+
await sandbox.exec("rm", "-rf", payload_tmp)
|
|
693
|
+
except Exception as e:
|
|
694
|
+
logger.warning(f"Best-effort cleanup of service payload staging failed (ignored): {e}")
|
|
695
|
+
|
|
696
|
+
files = [line.strip() for line in out.splitlines() if line.strip()]
|
|
697
|
+
if not files:
|
|
698
|
+
logger.warning(
|
|
699
|
+
f"EnvironmentArtifact '{environment_artifact.id}' v{environment_artifact.version} "
|
|
700
|
+
f"({file_artifact.filename}) produced no files — payload has no root/ tree "
|
|
701
|
+
f"and no files[] entries; is this a filesystem-style component?"
|
|
702
|
+
)
|
|
703
|
+
else:
|
|
704
|
+
logger.info(
|
|
705
|
+
f"Staged {len(files)} file(s) from EnvironmentArtifact '{environment_artifact.id}' "
|
|
706
|
+
f"v{environment_artifact.version} into "
|
|
707
|
+
f"{'the VM host' if container_name is None else repr(container_name)} at {destination}"
|
|
708
|
+
)
|
|
709
|
+
return files
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
async def _load_universe_onto_vm(sandbox, universe, destination: str) -> list[str]:
|
|
713
|
+
"""Pull each file in `universe` from S3 straight onto the VM host — no temp file, no copy inward."""
|
|
714
|
+
file_artifacts = universe.get_file_artifacts()
|
|
715
|
+
if not file_artifacts:
|
|
716
|
+
logger.warning(
|
|
717
|
+
f"FileArtifactUniverse '{universe.id}' v{universe.version} has no files; nothing to load"
|
|
718
|
+
)
|
|
719
|
+
return []
|
|
720
|
+
|
|
721
|
+
await sandbox.exec_script(f"mkdir -p {shlex.quote(destination)}")
|
|
722
|
+
loaded: list[str] = []
|
|
723
|
+
total = len(file_artifacts)
|
|
724
|
+
logger.info(
|
|
725
|
+
f"Loading FileArtifactUniverse '{universe.id}' v{universe.version} ({total} file(s)) "
|
|
726
|
+
f"onto the VM host at {destination}"
|
|
727
|
+
)
|
|
728
|
+
for idx, (filename, fa) in enumerate(file_artifacts.items(), 1):
|
|
729
|
+
validate_relative_filename(filename)
|
|
730
|
+
dest_path = posixpath.join(destination, filename)
|
|
731
|
+
parent = posixpath.dirname(dest_path)
|
|
732
|
+
logger.info(f" [{idx}/{total}] {fa.object_url} -> {dest_path}")
|
|
733
|
+
if parent and parent != destination:
|
|
734
|
+
await sandbox.exec_script(f"mkdir -p {shlex.quote(parent)}")
|
|
735
|
+
await sandbox.load_s3_file(fa.object_url, dest_path)
|
|
736
|
+
loaded.append(filename)
|
|
737
|
+
return loaded
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
async def _load_universe_into_container(sandbox, container_name: str, universe, destination: str) -> list[str]:
|
|
741
|
+
"""For each file in `universe`: pull from S3 to a VM temp path, then `docker cp` into `container_name` at `destination/<rel_path>`."""
|
|
742
|
+
file_artifacts = universe.get_file_artifacts()
|
|
743
|
+
if not file_artifacts:
|
|
744
|
+
logger.warning(
|
|
745
|
+
f"FileArtifactUniverse '{universe.id}' v{universe.version} has no files; nothing to load"
|
|
746
|
+
)
|
|
747
|
+
return []
|
|
748
|
+
|
|
749
|
+
await sandbox.exec_script(
|
|
750
|
+
f"sudo docker exec {shlex.quote(container_name)} mkdir -p {shlex.quote(destination)}"
|
|
751
|
+
)
|
|
752
|
+
loaded: list[str] = []
|
|
753
|
+
total = len(file_artifacts)
|
|
754
|
+
logger.info(
|
|
755
|
+
f"Loading FileArtifactUniverse '{universe.id}' v{universe.version} ({total} file(s)) "
|
|
756
|
+
f"into container '{container_name}' at {destination}"
|
|
757
|
+
)
|
|
758
|
+
for idx, (filename, fa) in enumerate(file_artifacts.items(), 1):
|
|
759
|
+
validate_relative_filename(filename)
|
|
760
|
+
dest_path = posixpath.join(destination, filename)
|
|
761
|
+
parent = posixpath.dirname(dest_path)
|
|
762
|
+
vm_temp = f"/tmp/_load_{uuid.uuid4().hex[:8]}_{filename.replace('/', '_')}"
|
|
763
|
+
logger.info(f" [{idx}/{total}] {fa.object_url} -> {container_name}:{dest_path}")
|
|
764
|
+
await sandbox.load_s3_file(fa.object_url, vm_temp)
|
|
765
|
+
if parent and parent != destination:
|
|
766
|
+
await sandbox.exec_script(
|
|
767
|
+
f"sudo docker exec {shlex.quote(container_name)} mkdir -p {shlex.quote(parent)}"
|
|
768
|
+
)
|
|
769
|
+
await sandbox.exec_script(
|
|
770
|
+
f"sudo docker cp {shlex.quote(vm_temp)} "
|
|
771
|
+
f"{shlex.quote(container_name)}:{shlex.quote(dest_path)} && "
|
|
772
|
+
f"rm -f {shlex.quote(vm_temp)}"
|
|
773
|
+
)
|
|
774
|
+
loaded.append(filename)
|
|
775
|
+
return loaded
|