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
agent_env/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""A2A Agent module for AgentEnv."""
|
|
2
|
+
from .a2a_agent import A2AAgent, DeployedA2AAgent, NegotiatedAgentConfig
|
|
3
|
+
from .store import (
|
|
4
|
+
A2AAgentInstanceStore, A2AAgentQuery, A2AAgentStore,
|
|
5
|
+
get_a2a_agent_instance_store, get_a2a_agent_store,
|
|
6
|
+
reset_a2a_agent_instance_store, reset_a2a_agent_store,
|
|
7
|
+
set_a2a_agent_instance_store, set_a2a_agent_store,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"A2AAgent",
|
|
12
|
+
"DeployedA2AAgent",
|
|
13
|
+
"NegotiatedAgentConfig",
|
|
14
|
+
"A2AAgentStore",
|
|
15
|
+
"A2AAgentQuery",
|
|
16
|
+
"A2AAgentInstanceStore",
|
|
17
|
+
"get_a2a_agent_store",
|
|
18
|
+
"set_a2a_agent_store",
|
|
19
|
+
"reset_a2a_agent_store",
|
|
20
|
+
"get_a2a_agent_instance_store",
|
|
21
|
+
"set_a2a_agent_instance_store",
|
|
22
|
+
"reset_a2a_agent_instance_store",
|
|
23
|
+
]
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""A2A Agent model for AgentEnv."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import posixpath
|
|
8
|
+
import shlex
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional
|
|
11
|
+
|
|
12
|
+
from agentenv_protocol.a2a_agent import STANDARD_EXTENSIONS
|
|
13
|
+
|
|
14
|
+
from agent_env.config import get_config
|
|
15
|
+
from agent_env.entity_refs import EntityRef
|
|
16
|
+
from agent_env.providers.sandbox_providers.sandbox import NetworkPolicy, NetworkPolicyUnsupportedError
|
|
17
|
+
from agent_env.artifact.artifacts.docker_image import DockerImageArtifact
|
|
18
|
+
from agent_env.a2a_agent.object_transfer import (
|
|
19
|
+
TRANSFER_TIMEOUT_SECONDS,
|
|
20
|
+
invoke_transfer,
|
|
21
|
+
skill_add_call,
|
|
22
|
+
)
|
|
23
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import all_sandbox_container_env, all_sandbox_url_rewrites
|
|
24
|
+
from agent_env.utils.paths import validate_relative_filename
|
|
25
|
+
from agent_env.attribution import Attribution
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from agent_env.a2a_agent.store import A2AAgentQuery
|
|
29
|
+
from agent_env.bundle.authoring import AuthoringContext
|
|
30
|
+
from agent_env.artifact import CliArtifact
|
|
31
|
+
from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
|
|
32
|
+
from agent_env.providers.sandbox_providers.sandbox import Sandbox
|
|
33
|
+
from agent_env.task_step.task_steps.add_skills import Skill
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
DEFAULT_A2A_PORT = 8000
|
|
38
|
+
AGENT_CARD_TIMEOUT = 300
|
|
39
|
+
|
|
40
|
+
# Rootless (not the VM socket), so a container escape is an unprivileged VM user,
|
|
41
|
+
# not VM root. Digest-pinned because the sidecar runs --privileged.
|
|
42
|
+
_DIND_IMAGE = (
|
|
43
|
+
"public.ecr.aws/docker/library/docker:27-dind-rootless"
|
|
44
|
+
"@sha256:e2ac8e8f66ae21a060b0a8e3005c70f6ed9441aabf409434463d1f6eecd38026"
|
|
45
|
+
)
|
|
46
|
+
_DIND_CONTAINER = "agent-dind"
|
|
47
|
+
_DIND_NETWORK = "agent-docker-net"
|
|
48
|
+
_DIND_PORT = 2375
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DeployedA2AAgent:
|
|
53
|
+
agent_id: str
|
|
54
|
+
agent_version: int
|
|
55
|
+
a2a_url: str
|
|
56
|
+
sandbox_id: str
|
|
57
|
+
agent_card: dict
|
|
58
|
+
sandbox_type: str | None = None
|
|
59
|
+
instance_id: str | None = None
|
|
60
|
+
created_at_utc: str | None = None
|
|
61
|
+
expires_at_utc: str | None = None
|
|
62
|
+
network_policy: dict | None = None
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dict(cls, data: dict) -> "DeployedA2AAgent":
|
|
66
|
+
return cls(
|
|
67
|
+
agent_id=data["agent_id"],
|
|
68
|
+
agent_version=data["agent_version"],
|
|
69
|
+
a2a_url=data["a2a_url"],
|
|
70
|
+
sandbox_id=data["sandbox_id"],
|
|
71
|
+
agent_card=data.get("agent_card") or {},
|
|
72
|
+
sandbox_type=data.get("sandbox_type"),
|
|
73
|
+
instance_id=data.get("instance_id"),
|
|
74
|
+
created_at_utc=data.get("created_at_utc"),
|
|
75
|
+
expires_at_utc=data.get("expires_at_utc"),
|
|
76
|
+
network_policy=data.get("network_policy"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class NegotiatedAgentConfig:
|
|
82
|
+
endpoint: str
|
|
83
|
+
fields: dict
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class A2AAgent:
|
|
87
|
+
type: ClassVar[str] = "a2a_agent"
|
|
88
|
+
toml_refs: ClassVar[tuple[EntityRef, ...]] = (EntityRef.artifact("image", artifact_type="docker_image"),)
|
|
89
|
+
toml_keys: ClassVar[dict[str, type]] = {"image": object, "default_env_vars": dict, "metadata": dict}
|
|
90
|
+
# What an agent.toml's [metadata] may set: the types each key takes, and how a problem names them.
|
|
91
|
+
toml_metadata: ClassVar[dict[str, tuple[tuple[type, ...], str]]] = {
|
|
92
|
+
"default_model": ((str,), "a string"), "min_disk_size_gb": ((int, float), "a number"),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
EXT_MCP_CONFIG: ClassVar[str] = "urn:agentenv:mcp-config/v1"
|
|
96
|
+
EXT_SKILL_CONFIG: ClassVar[str] = "urn:agentenv:skill-config/v1"
|
|
97
|
+
EXT_TRAJECTORY: ClassVar[str] = "urn:agentenv:trajectory/v1"
|
|
98
|
+
EXT_AGENT_CONFIG: ClassVar[str] = "urn:agentenv:agent-config/v1"
|
|
99
|
+
EXT_SNAPSHOT: ClassVar[str] = "urn:agentenv:snapshot/v1"
|
|
100
|
+
EXT_PEER_AGENTS: ClassVar[str] = "urn:agentenv:peer-agents/v1"
|
|
101
|
+
EXT_INSTALL: ClassVar[str] = "urn:agentenv:install/v1"
|
|
102
|
+
EXT_TOOLS: ClassVar[str] = "urn:agentenv:tools/v1"
|
|
103
|
+
EXT_TRIGGERS: ClassVar[str] = "urn:agentenv:triggers/v1"
|
|
104
|
+
|
|
105
|
+
# Whole-writable-layer changelog (capture + reconstruct) is exposed as two
|
|
106
|
+
# methods on the snapshot extension, so capability is per-method via
|
|
107
|
+
# extension_method() rather than a separate extension.
|
|
108
|
+
SNAPSHOT_METHOD_ENABLE_CHANGELOG: ClassVar[str] = "enable-changelog"
|
|
109
|
+
SNAPSHOT_METHOD_APPLY_CHANGELOG: ClassVar[str] = "apply-changelog"
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def find_extension(card: dict, uri: str) -> dict | None:
|
|
113
|
+
caps = card.get("capabilities") or {}
|
|
114
|
+
for ext in caps.get("extensions", []):
|
|
115
|
+
if ext.get("uri") == uri:
|
|
116
|
+
return ext
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def extension_method(card: dict, uri: str, method: str) -> dict | None:
|
|
121
|
+
"""Return an advertised extension method, or ``None`` if absent."""
|
|
122
|
+
return A2AAgent.operation(A2AAgent.find_extension(card, uri), method)[0]
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def operation(extension: dict | None, name: str) -> tuple[dict | None, str | None]:
|
|
126
|
+
"""An advertised extension's ``name`` method (None when unlisted) and its endpoint path.
|
|
127
|
+
|
|
128
|
+
As the protocol renders cards: a method's own endpoint wins; otherwise one the protocol
|
|
129
|
+
serves at the extension's path uses the card's extension endpoint, and one it serves
|
|
130
|
+
elsewhere uses the protocol's path for it."""
|
|
131
|
+
if extension is None:
|
|
132
|
+
return None, None
|
|
133
|
+
config = extension.get("config") or extension.get("params") or {}
|
|
134
|
+
method = (config.get("methods") or {}).get(name)
|
|
135
|
+
if method and method.get("endpoint"):
|
|
136
|
+
return method, method["endpoint"]
|
|
137
|
+
definition = STANDARD_EXTENSIONS.get(extension.get("uri"))
|
|
138
|
+
spec = next(
|
|
139
|
+
(op for op in (definition.operations.values() if definition else ()) if op.name == name),
|
|
140
|
+
None,
|
|
141
|
+
)
|
|
142
|
+
if spec is not None and spec.path != definition.endpoint:
|
|
143
|
+
return method, spec.path
|
|
144
|
+
return method, config.get("endpoint") or (spec.path if spec else None)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def negotiate_agent_config(card: dict, desired: dict) -> Optional[NegotiatedAgentConfig]:
|
|
148
|
+
"""Filter ``desired`` to the fields the card's agent-config extension accepts (capability negotiation)."""
|
|
149
|
+
ext = A2AAgent.find_extension(card, A2AAgent.EXT_AGENT_CONFIG)
|
|
150
|
+
if not ext:
|
|
151
|
+
return None
|
|
152
|
+
params = ext.get("params") or {}
|
|
153
|
+
supported = params.get("methods", {}).get("set", {}).get("request", {}).get("supported", [])
|
|
154
|
+
fields = {k: v for k, v in desired.items() if k in supported}
|
|
155
|
+
if not fields:
|
|
156
|
+
return None
|
|
157
|
+
return NegotiatedAgentConfig(params.get("endpoint", "/ext/agent-config"), fields)
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
async def add_skill(deployed: "DeployedA2AAgent", skill: "Skill") -> dict:
|
|
161
|
+
"""Validate ``skill`` and register it on the deployed agent."""
|
|
162
|
+
A2AAgent._skill_extension(deployed)
|
|
163
|
+
await asyncio.to_thread(skill.validate)
|
|
164
|
+
if skill.skill_artifact_id is not None:
|
|
165
|
+
from agent_env.artifact import SkillArtifact
|
|
166
|
+
artifact = SkillArtifact.get(skill.skill_artifact_id, skill.skill_artifact_version)
|
|
167
|
+
return await A2AAgent.register_skill(
|
|
168
|
+
deployed,
|
|
169
|
+
name=artifact.skill_name,
|
|
170
|
+
description=artifact.description,
|
|
171
|
+
object_url=artifact.skill_object_url,
|
|
172
|
+
)
|
|
173
|
+
if skill.s3_url is not None:
|
|
174
|
+
return await A2AAgent.register_skill(
|
|
175
|
+
deployed, name=skill.name, description=skill.description, object_url=skill.s3_url
|
|
176
|
+
)
|
|
177
|
+
return await A2AAgent.register_skill(
|
|
178
|
+
deployed, name=skill.name, description=skill.description, skill_md=skill.to_skill_md()
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
async def register_skill(
|
|
183
|
+
deployed: "DeployedA2AAgent",
|
|
184
|
+
*,
|
|
185
|
+
name: str,
|
|
186
|
+
description: str,
|
|
187
|
+
skill_md: str | None = None,
|
|
188
|
+
object_url: str | None = None,
|
|
189
|
+
) -> dict:
|
|
190
|
+
"""Send one skill, as SKILL.md text or the objects under ``object_url``, in a form the
|
|
191
|
+
agent's card and the object store allow; returns the agent's answer."""
|
|
192
|
+
add_method, add_path = A2AAgent.operation(A2AAgent._skill_extension(deployed), "add")
|
|
193
|
+
call = await asyncio.to_thread(
|
|
194
|
+
skill_add_call,
|
|
195
|
+
add_method,
|
|
196
|
+
get_config().get_object_store(),
|
|
197
|
+
name=name,
|
|
198
|
+
description=description,
|
|
199
|
+
skill_md=skill_md,
|
|
200
|
+
object_url=object_url,
|
|
201
|
+
)
|
|
202
|
+
return await invoke_transfer(
|
|
203
|
+
deployed.a2a_url + add_path,
|
|
204
|
+
call,
|
|
205
|
+
verb="POST",
|
|
206
|
+
operation=f"skill add ({name})",
|
|
207
|
+
timeout=TRANSFER_TIMEOUT_SECONDS,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _skill_extension(deployed: "DeployedA2AAgent") -> dict:
|
|
212
|
+
skill_ext = A2AAgent.find_extension(deployed.agent_card, A2AAgent.EXT_SKILL_CONFIG)
|
|
213
|
+
if not skill_ext:
|
|
214
|
+
raise RuntimeError(
|
|
215
|
+
f"Deployed agent {deployed.instance_id or deployed.agent_id} does not advertise {A2AAgent.EXT_SKILL_CONFIG}"
|
|
216
|
+
)
|
|
217
|
+
return skill_ext
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
async def install_cli(deployed: "DeployedA2AAgent", cli_artifact: "CliArtifact", gateway_url: str) -> str:
|
|
221
|
+
"""Install a CliArtifact bundle into the agent's filesystem at /opt/cli/<command>/.
|
|
222
|
+
Writes a sidecar bin/.env with the gateway URL and chmod +x's the entrypoint.
|
|
223
|
+
Idempotent. Returns the in-process path to the executable entrypoint."""
|
|
224
|
+
from agent_env.providers.sandbox_providers.sandbox import VmSandbox
|
|
225
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import build_sandbox_provider, get_agent_sandbox_provider
|
|
226
|
+
provider = build_sandbox_provider(deployed.sandbox_type) if deployed.sandbox_type else get_agent_sandbox_provider()
|
|
227
|
+
sandbox = await provider.get_sandbox(deployed.sandbox_id)
|
|
228
|
+
target_root = f"/opt/cli/{cli_artifact.command_name}"
|
|
229
|
+
entrypoint_path = f"{target_root}/{cli_artifact.entrypoint}"
|
|
230
|
+
env_dir = posixpath.dirname(entrypoint_path)
|
|
231
|
+
|
|
232
|
+
# write .env first — write_file_from_text creates the parent dir; write_file_from_s3 does not.
|
|
233
|
+
await sandbox.write_file_from_text(f"AGENT_ENV_GATEWAY_URL={gateway_url}\n", f"{env_dir}/.env")
|
|
234
|
+
|
|
235
|
+
for rel_path, file_artifact in cli_artifact.get_cli_files().get_file_artifacts().items():
|
|
236
|
+
await sandbox.write_file_from_s3(file_artifact.object_url, f"{target_root}/{rel_path}")
|
|
237
|
+
|
|
238
|
+
if isinstance(sandbox, VmSandbox):
|
|
239
|
+
await sandbox.exec_script(f"docker exec -u 0 {shlex.quote(sandbox.container_name)} chmod +x {entrypoint_path}")
|
|
240
|
+
else:
|
|
241
|
+
await sandbox.exec("chmod", "+x", entrypoint_path)
|
|
242
|
+
|
|
243
|
+
logger.info(f"Installed CliArtifact '{cli_artifact.id}' v{cli_artifact.version} at {entrypoint_path}")
|
|
244
|
+
return entrypoint_path
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
async def load_file_artifact_universe(
|
|
248
|
+
deployed: "DeployedA2AAgent",
|
|
249
|
+
universe: "FileArtifactUniverse",
|
|
250
|
+
destination_path: str,
|
|
251
|
+
) -> dict[str, str]:
|
|
252
|
+
"""Stage every FileArtifact in `universe` into the agent sandbox under destination_path."""
|
|
253
|
+
from agent_env.providers.sandbox_providers.sandbox import VmSandbox
|
|
254
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import build_sandbox_provider, get_agent_sandbox_provider
|
|
255
|
+
|
|
256
|
+
destination = destination_path.rstrip("/") or "/"
|
|
257
|
+
provider = build_sandbox_provider(deployed.sandbox_type) if deployed.sandbox_type else get_agent_sandbox_provider()
|
|
258
|
+
sandbox = await provider.get_sandbox(deployed.sandbox_id)
|
|
259
|
+
|
|
260
|
+
file_artifacts = universe.get_file_artifacts()
|
|
261
|
+
if not file_artifacts:
|
|
262
|
+
logger.warning(
|
|
263
|
+
f"FileArtifactUniverse '{universe.id}' v{universe.version} has no files; nothing to load into agent"
|
|
264
|
+
)
|
|
265
|
+
return {}
|
|
266
|
+
|
|
267
|
+
dirs_to_make: set[str] = {destination}
|
|
268
|
+
loaded: dict[str, str] = {}
|
|
269
|
+
for filename in file_artifacts:
|
|
270
|
+
validate_relative_filename(filename)
|
|
271
|
+
dest_path = posixpath.join(destination, filename)
|
|
272
|
+
parent = posixpath.dirname(dest_path)
|
|
273
|
+
if parent:
|
|
274
|
+
dirs_to_make.add(parent)
|
|
275
|
+
loaded[filename] = dest_path
|
|
276
|
+
|
|
277
|
+
for d in sorted(dirs_to_make):
|
|
278
|
+
if isinstance(sandbox, VmSandbox):
|
|
279
|
+
await sandbox.exec_script(f"docker exec -u 0 {shlex.quote(sandbox.container_name)} mkdir -p {shlex.quote(d)}")
|
|
280
|
+
else:
|
|
281
|
+
await sandbox.exec("mkdir", "-p", d)
|
|
282
|
+
|
|
283
|
+
total = len(file_artifacts)
|
|
284
|
+
logger.info(
|
|
285
|
+
f"Loading FileArtifactUniverse '{universe.id}' v{universe.version} "
|
|
286
|
+
f"({total} file(s)) into agent at {destination}"
|
|
287
|
+
)
|
|
288
|
+
# Stage files in parallel with a bounded semaphore so a universe with
|
|
289
|
+
# many files doesn't accumulate per-file presign + docker cp latency.
|
|
290
|
+
# 8 matches the default concurrency used by the FAU `get-many` CLI.
|
|
291
|
+
sem = asyncio.Semaphore(8)
|
|
292
|
+
|
|
293
|
+
async def _stage(filename: str, file_artifact: "Any") -> None:
|
|
294
|
+
dest_path = loaded[filename]
|
|
295
|
+
async with sem:
|
|
296
|
+
logger.info(f" {file_artifact.object_url} -> {dest_path}")
|
|
297
|
+
await sandbox.write_file_from_s3(file_artifact.object_url, dest_path)
|
|
298
|
+
|
|
299
|
+
await asyncio.gather(*[_stage(fn, fa) for fn, fa in file_artifacts.items()])
|
|
300
|
+
|
|
301
|
+
return loaded
|
|
302
|
+
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
id: str,
|
|
306
|
+
version: Optional[int],
|
|
307
|
+
docker_image_artifact: DockerImageArtifact,
|
|
308
|
+
default_env_vars: dict[str, str] | None = None,
|
|
309
|
+
metadata: dict[str, Any] | None = None,
|
|
310
|
+
):
|
|
311
|
+
self.id = id
|
|
312
|
+
self.version = version
|
|
313
|
+
self.docker_image_artifact = docker_image_artifact
|
|
314
|
+
self.default_env_vars = default_env_vars or {}
|
|
315
|
+
self.metadata = metadata or {}
|
|
316
|
+
self._sandbox: Sandbox | None = None
|
|
317
|
+
self._owns_sandbox: bool = True
|
|
318
|
+
|
|
319
|
+
def to_dict(self) -> dict:
|
|
320
|
+
return {
|
|
321
|
+
"id": self.id,
|
|
322
|
+
"type": self.type,
|
|
323
|
+
"version": self.version,
|
|
324
|
+
"docker_image_artifact": {
|
|
325
|
+
"id": self.docker_image_artifact.id,
|
|
326
|
+
"version": self.docker_image_artifact.version,
|
|
327
|
+
"type": self.docker_image_artifact.type,
|
|
328
|
+
},
|
|
329
|
+
"default_env_vars": self.default_env_vars,
|
|
330
|
+
"metadata": self.metadata,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_dict(cls, data: dict) -> A2AAgent:
|
|
335
|
+
from agent_env.artifact import Artifact
|
|
336
|
+
artifact_ref = data["docker_image_artifact"]
|
|
337
|
+
docker_image_artifact = Artifact.get(artifact_ref["id"], version=artifact_ref["version"])
|
|
338
|
+
metadata = data.get("metadata", {})
|
|
339
|
+
# Migrate top-level fields from older documents into metadata
|
|
340
|
+
for key in ("min_disk_size_gb", "default_model"):
|
|
341
|
+
if key in data and data[key] is not None and key not in metadata:
|
|
342
|
+
metadata[key] = data[key]
|
|
343
|
+
return cls(
|
|
344
|
+
id=data["id"], version=data.get("version"),
|
|
345
|
+
docker_image_artifact=docker_image_artifact,
|
|
346
|
+
default_env_vars=data.get("default_env_vars", {}),
|
|
347
|
+
metadata=metadata,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
@classmethod
|
|
351
|
+
def from_toml(cls, data: dict[str, Any], ctx: AuthoringContext) -> A2AAgent:
|
|
352
|
+
"""Write the agent authored as ``data`` (its agent.toml, with ``image`` resolved to an image
|
|
353
|
+
artifact's id) under ``ctx.id`` and return it. The card isn't authored: the image serves it."""
|
|
354
|
+
fields = cls.accept_toml(data, ctx)
|
|
355
|
+
return cls.put(id=ctx.id, docker_image_artifact=ctx.artifact(fields["image"], DockerImageArtifact),
|
|
356
|
+
default_env_vars=fields.get("default_env_vars"), metadata=fields.get("metadata"))
|
|
357
|
+
|
|
358
|
+
@classmethod
|
|
359
|
+
def accept_toml(cls, data: dict[str, Any], ctx: AuthoringContext) -> dict[str, Any]:
|
|
360
|
+
"""The keys of ``data``, an agent.toml, that an agent takes, each checked the way ``from_toml``
|
|
361
|
+
needs it. Raises BundleError listing every problem."""
|
|
362
|
+
fields, problems = ctx.accepted(data, **cls.toml_keys)
|
|
363
|
+
values = [f"default_env_vars.{name} must be a string, not {value!r}"
|
|
364
|
+
for name, value in fields.get("default_env_vars", {}).items() if not isinstance(value, str)]
|
|
365
|
+
for name, value in fields.get("metadata", {}).items():
|
|
366
|
+
if name not in cls.toml_metadata:
|
|
367
|
+
values.append(f"unknown [metadata] key {name!r}; an agent's [metadata] takes "
|
|
368
|
+
f"{' and '.join(cls.toml_metadata)}")
|
|
369
|
+
continue
|
|
370
|
+
kinds, described = cls.toml_metadata[name]
|
|
371
|
+
if isinstance(value, bool) or not isinstance(value, kinds):
|
|
372
|
+
values.append(f"metadata.{name} must be {described}, not {value!r}")
|
|
373
|
+
problems += [f"agent.toml: {problem}" for problem in values]
|
|
374
|
+
if problems:
|
|
375
|
+
ctx.refuse(problems)
|
|
376
|
+
return fields
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def get(cls, id: str, version: Optional[int] = None) -> A2AAgent:
|
|
380
|
+
from .store import get_a2a_agent_store
|
|
381
|
+
return get_a2a_agent_store().get(id, version)
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def put(cls, **kwargs) -> A2AAgent:
|
|
385
|
+
from .store import get_a2a_agent_store
|
|
386
|
+
kwargs.setdefault("version", None)
|
|
387
|
+
instance = cls(**kwargs)
|
|
388
|
+
return get_a2a_agent_store().put_document(instance)
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def query(cls) -> A2AAgentQuery:
|
|
392
|
+
from .store import A2AAgentQuery, get_a2a_agent_store
|
|
393
|
+
return A2AAgentQuery(get_a2a_agent_store())
|
|
394
|
+
|
|
395
|
+
VALIDATION_ENV_ID: ClassVar[str] = "multi-caf64d"
|
|
396
|
+
|
|
397
|
+
def update_metadata(self, new_metadata: dict[str, Any]) -> None:
|
|
398
|
+
"""Replace this agent's metadata in the database using compare-and-swap."""
|
|
399
|
+
from .store import get_a2a_agent_store
|
|
400
|
+
if self.version is None:
|
|
401
|
+
raise ValueError("Cannot update metadata on an unsaved agent (version is None)")
|
|
402
|
+
self.metadata = get_a2a_agent_store().update_metadata(self.id, self.version, self.metadata, new_metadata)
|
|
403
|
+
|
|
404
|
+
async def validate(self, on_progress: Callable[[str], None] | None = None, litellm_api_key: str | None = None) -> dict:
|
|
405
|
+
from agent_env.a2a_agent.validator import A2AAgentValidator
|
|
406
|
+
return await A2AAgentValidator.validate(self, on_progress=on_progress, litellm_api_key=litellm_api_key)
|
|
407
|
+
|
|
408
|
+
async def deploy(
|
|
409
|
+
self,
|
|
410
|
+
ttl_seconds: int = 7200,
|
|
411
|
+
env_vars: dict[str, str] | None = None,
|
|
412
|
+
a2a_port: int = DEFAULT_A2A_PORT,
|
|
413
|
+
cpu: float = 1.0,
|
|
414
|
+
memory: int = 8192,
|
|
415
|
+
disk_size_gb: float | None = None,
|
|
416
|
+
sandbox_type: str | None = None,
|
|
417
|
+
sandbox: Sandbox | None = None,
|
|
418
|
+
enable_docker: bool = False,
|
|
419
|
+
priority: Optional[int] = None,
|
|
420
|
+
network_policy: NetworkPolicy | None = None,
|
|
421
|
+
*,
|
|
422
|
+
attribution: Optional[Attribution] = None,
|
|
423
|
+
) -> DeployedA2AAgent:
|
|
424
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import (
|
|
425
|
+
SANDBOX_MODE_VM,
|
|
426
|
+
build_sandbox_provider,
|
|
427
|
+
get_agent_sandbox_provider,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
attribution = dict(attribution or {})
|
|
431
|
+
if disk_size_gb is None:
|
|
432
|
+
disk_size_gb = float(self.metadata.get("min_disk_size_gb") or 10)
|
|
433
|
+
|
|
434
|
+
config = get_config()
|
|
435
|
+
resolved_env = dict(env_vars) if env_vars else {}
|
|
436
|
+
if "LITELLM_API_KEY" not in resolved_env and "LITELLM_API_KEY" not in self.default_env_vars:
|
|
437
|
+
resolved_env["LITELLM_API_KEY"] = config.get_litellm_api_key()
|
|
438
|
+
if "LITELLM_BASE_URL" not in resolved_env and "LITELLM_BASE_URL" not in self.default_env_vars:
|
|
439
|
+
resolved_env["LITELLM_BASE_URL"] = config.get_litellm_base_url()
|
|
440
|
+
|
|
441
|
+
merged_env = await asyncio.to_thread(self._build_merged_env, resolved_env, a2a_port)
|
|
442
|
+
image_name = self.docker_image_artifact.image_name
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
if sandbox is None:
|
|
446
|
+
provider = build_sandbox_provider(sandbox_type) if sandbox_type else get_agent_sandbox_provider()
|
|
447
|
+
logger.info(f"Provisioning sandbox for A2A agent '{self.id}' via {type(provider).__name__}...")
|
|
448
|
+
self._sandbox = await provider.create_sandbox(
|
|
449
|
+
image_name=image_name, port=a2a_port, env=merged_env,
|
|
450
|
+
cpu=cpu, memory=memory, disk_size_gb=disk_size_gb, timeout=ttl_seconds,
|
|
451
|
+
attribution=attribution,
|
|
452
|
+
priority=priority, network_policy=network_policy,
|
|
453
|
+
)
|
|
454
|
+
self._owns_sandbox = True
|
|
455
|
+
logger.info(f"Sandbox created: {self._sandbox.sandbox_id} (mode={self._sandbox.mode})")
|
|
456
|
+
else:
|
|
457
|
+
if network_policy is not None and network_policy.restricts_egress:
|
|
458
|
+
raise NetworkPolicyUnsupportedError(
|
|
459
|
+
f"network_policy={network_policy.mode.value!r} cannot be applied to a pre-existing "
|
|
460
|
+
f"sandbox {sandbox.sandbox_id!r}; deploy the agent on its own sandbox instead"
|
|
461
|
+
)
|
|
462
|
+
self._sandbox = sandbox
|
|
463
|
+
self._owns_sandbox = False
|
|
464
|
+
logger.info(f"Using pre-existing sandbox {self._sandbox.sandbox_id} (mode={self._sandbox.mode}) for A2A agent '{self.id}'")
|
|
465
|
+
|
|
466
|
+
if enable_docker and self._sandbox.mode != SANDBOX_MODE_VM:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"enable_docker=True requires a VM sandbox, but the agent sandbox mode "
|
|
469
|
+
f"is {self._sandbox.mode!r} (Docker-in-Docker is VM-only)"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if self._sandbox.mode == SANDBOX_MODE_VM:
|
|
473
|
+
logger.info("Loading Docker image into VM...")
|
|
474
|
+
await self._sandbox.load_docker_images([self.docker_image_artifact])
|
|
475
|
+
logger.info("Starting A2A agent container...")
|
|
476
|
+
await self._run_container(image_name, a2a_port, merged_env, enable_docker=enable_docker)
|
|
477
|
+
|
|
478
|
+
a2a_url = self._sandbox.tunnel_urls.get(a2a_port)
|
|
479
|
+
if not a2a_url:
|
|
480
|
+
raise RuntimeError(f"No tunnel URL for port {a2a_port}")
|
|
481
|
+
|
|
482
|
+
logger.info("Waiting for agent card...")
|
|
483
|
+
agent_card = await self._wait_for_agent_card(a2a_url)
|
|
484
|
+
|
|
485
|
+
deployed = DeployedA2AAgent(
|
|
486
|
+
agent_id=self.id, agent_version=self.version,
|
|
487
|
+
a2a_url=a2a_url, sandbox_id=self._sandbox.sandbox_id,
|
|
488
|
+
agent_card=agent_card, sandbox_type=self._sandbox.type,
|
|
489
|
+
network_policy=self._sandbox.network_policy.to_dict() if self._sandbox.network_policy else None,
|
|
490
|
+
)
|
|
491
|
+
from agent_env.a2a_agent.store import get_a2a_agent_instance_store
|
|
492
|
+
deployed = get_a2a_agent_instance_store().create_instance(deployed, ttl_seconds)
|
|
493
|
+
logger.info(f"A2A agent '{self.id}' deployed at {a2a_url} (instance={deployed.instance_id})")
|
|
494
|
+
return deployed
|
|
495
|
+
except BaseException: # a cancelled deploy closes its sandbox too
|
|
496
|
+
await self.close()
|
|
497
|
+
raise
|
|
498
|
+
|
|
499
|
+
def _build_merged_env(self, resolved_env: dict[str, str], a2a_port: int) -> dict[str, str]:
|
|
500
|
+
merged_env = dict(self.default_env_vars)
|
|
501
|
+
merged_env.update(resolved_env)
|
|
502
|
+
merged_env["A2A_PORT"] = str(a2a_port)
|
|
503
|
+
shared = get_config().get_object_store().shared_credentials_env()
|
|
504
|
+
if shared.keys().isdisjoint(merged_env):
|
|
505
|
+
merged_env.update(shared)
|
|
506
|
+
|
|
507
|
+
merged_env.update(all_sandbox_container_env())
|
|
508
|
+
rewrites = all_sandbox_url_rewrites()
|
|
509
|
+
if rewrites:
|
|
510
|
+
merged_env["SANDBOX_URL_REWRITES"] = json.dumps(rewrites, sort_keys=True)
|
|
511
|
+
return merged_env
|
|
512
|
+
|
|
513
|
+
async def close(self) -> None:
|
|
514
|
+
if self._sandbox is not None and self._owns_sandbox:
|
|
515
|
+
try:
|
|
516
|
+
await self._sandbox.terminate()
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.warning(f"Error terminating sandbox: {e}")
|
|
519
|
+
self._sandbox = None
|
|
520
|
+
|
|
521
|
+
async def _run_container(self, image_name: str, a2a_port: int, merged_env: dict[str, str], enable_docker: bool = False) -> None:
|
|
522
|
+
agent_env = dict(merged_env)
|
|
523
|
+
setup_script = ""
|
|
524
|
+
network_flag = ""
|
|
525
|
+
if enable_docker:
|
|
526
|
+
logger.info("enable_docker: starting rootless Docker-in-Docker sidecar for the agent (no host socket)")
|
|
527
|
+
setup_script = self._dind_setup_script()
|
|
528
|
+
network_flag = f"--network {_DIND_NETWORK} \\\n "
|
|
529
|
+
agent_env["DOCKER_HOST"] = f"tcp://{_DIND_CONTAINER}:{_DIND_PORT}"
|
|
530
|
+
|
|
531
|
+
env_flags = []
|
|
532
|
+
for key, value in agent_env.items():
|
|
533
|
+
escaped_value = value.replace("'", "'\\''")
|
|
534
|
+
env_flags.append(f"-e {key}='{escaped_value}'")
|
|
535
|
+
env_str = " \\\n ".join(env_flags)
|
|
536
|
+
|
|
537
|
+
run_script = f"""#!/bin/bash
|
|
538
|
+
set -e
|
|
539
|
+
{setup_script}docker run -d \\
|
|
540
|
+
--name {self._sandbox.container_name} \\
|
|
541
|
+
-p {a2a_port}:{a2a_port} \\
|
|
542
|
+
{network_flag}{env_str} \\
|
|
543
|
+
{image_name} > /dev/null
|
|
544
|
+
sleep 2
|
|
545
|
+
"""
|
|
546
|
+
await self._sandbox.exec_script(run_script)
|
|
547
|
+
|
|
548
|
+
def _dind_setup_script(self) -> str:
|
|
549
|
+
# Constants become shell vars so the body stays a raw string (no f-string
|
|
550
|
+
# brace escaping). TLS off is safe: the tcp endpoint is only reachable on
|
|
551
|
+
# the VM-local bridge.
|
|
552
|
+
env = (
|
|
553
|
+
f"DIND_IMAGE={_DIND_IMAGE}\n"
|
|
554
|
+
f"DIND_CONTAINER={_DIND_CONTAINER}\n"
|
|
555
|
+
f"DIND_NETWORK={_DIND_NETWORK}\n"
|
|
556
|
+
f"DIND_PORT={_DIND_PORT}\n"
|
|
557
|
+
)
|
|
558
|
+
return env + r"""docker network create "$DIND_NETWORK" >/dev/null 2>&1 || true
|
|
559
|
+
docker rm -f "$DIND_CONTAINER" >/dev/null 2>&1 || true
|
|
560
|
+
docker run -d \
|
|
561
|
+
--name "$DIND_CONTAINER" \
|
|
562
|
+
--privileged \
|
|
563
|
+
--network "$DIND_NETWORK" \
|
|
564
|
+
-e DOCKER_TLS_CERTDIR="" \
|
|
565
|
+
"$DIND_IMAGE" > /dev/null
|
|
566
|
+
echo "waiting for rootless docker daemon..."
|
|
567
|
+
for i in $(seq 1 60); do
|
|
568
|
+
if docker exec "$DIND_CONTAINER" docker -H "tcp://localhost:$DIND_PORT" info >/dev/null 2>&1; then
|
|
569
|
+
echo "rootless docker daemon ready"
|
|
570
|
+
break
|
|
571
|
+
fi
|
|
572
|
+
if [ "$i" = "60" ]; then
|
|
573
|
+
echo "rootless docker daemon did not become ready" >&2
|
|
574
|
+
docker logs --tail 60 "$DIND_CONTAINER" >&2 2>&1 || true
|
|
575
|
+
exit 1
|
|
576
|
+
fi
|
|
577
|
+
sleep 2
|
|
578
|
+
done
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
async def _wait_for_agent_card(self, base_url: str) -> dict:
|
|
582
|
+
import httpx
|
|
583
|
+
card_url = f"{base_url}/.well-known/agent.json"
|
|
584
|
+
async with httpx.AsyncClient(timeout=2.0) as client:
|
|
585
|
+
for i in range(AGENT_CARD_TIMEOUT):
|
|
586
|
+
try:
|
|
587
|
+
resp = await client.get(card_url)
|
|
588
|
+
if resp.status_code == 200 and resp.text.strip():
|
|
589
|
+
card = resp.json()
|
|
590
|
+
logger.info(f"Agent card received: name={card.get('name')}")
|
|
591
|
+
return card
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
if i > 0 and i % 10 == 0:
|
|
595
|
+
logger.info(f"Waiting for agent card... ({i}s)")
|
|
596
|
+
await asyncio.sleep(1)
|
|
597
|
+
raise RuntimeError(
|
|
598
|
+
f"A2A agent '{self.id}' did not serve /.well-known/agent.json "
|
|
599
|
+
f"within {AGENT_CARD_TIMEOUT}s"
|
|
600
|
+
)
|