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,108 @@
|
|
|
1
|
+
"""Verify core environment-protocol operation support (env mirror of VerifyCoreA2AProtocolStep)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from agentenv_protocol import METHOD_ADD, METHOD_GET, METHOD_RESET, RPC_PATH
|
|
10
|
+
|
|
11
|
+
from agent_env.task_step.context import TaskStepContext
|
|
12
|
+
from agent_env.entity_refs import EntityRef
|
|
13
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# (method, probe_params); None = no mutation-free probe — resolve from the advertised operations.
|
|
18
|
+
_CORE_OPERATIONS = [
|
|
19
|
+
(METHOD_RESET, None),
|
|
20
|
+
(METHOD_ADD, {"parts": []}),
|
|
21
|
+
(METHOD_GET, {}),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
VALIDATED_ENVIRONMENT_PROTOCOL_KEY = "validated_environment_protocol"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VerifyCoreEnvironmentProtocolStep(TaskStep):
|
|
28
|
+
type: ClassVar[str] = "verify_env_core_protocol"
|
|
29
|
+
entity_refs = (EntityRef.env("env_id"),)
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
id: str,
|
|
34
|
+
version: Optional[int],
|
|
35
|
+
env_id: str,
|
|
36
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
37
|
+
fail_task_on_error: bool = True,
|
|
38
|
+
):
|
|
39
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
40
|
+
self.env_id = env_id
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
base = super().to_dict()
|
|
44
|
+
base["env_id"] = self.env_id
|
|
45
|
+
return base
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: dict) -> VerifyCoreEnvironmentProtocolStep:
|
|
49
|
+
return cls(**cls._base_from_dict(data), env_id=data["env_id"])
|
|
50
|
+
|
|
51
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
52
|
+
from agentenv_protocol import client as protocol_v1
|
|
53
|
+
from agent_env.env.env import Env
|
|
54
|
+
|
|
55
|
+
deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
|
|
56
|
+
if deployed is None:
|
|
57
|
+
raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
|
|
58
|
+
|
|
59
|
+
card = None
|
|
60
|
+
try:
|
|
61
|
+
card = await protocol_v1.get_card(deployed.environment_url)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.warning(f"env card for '{self.env_id}' inaccessible at {deployed.environment_url}: {type(e).__name__}: {e}")
|
|
64
|
+
|
|
65
|
+
protocol = {}
|
|
66
|
+
for method, probe_params in _CORE_OPERATIONS:
|
|
67
|
+
if probe_params is not None:
|
|
68
|
+
supported = await self._probe_operation(deployed.environment_url, method, probe_params)
|
|
69
|
+
else:
|
|
70
|
+
supported = self._advertised_support(card, method)
|
|
71
|
+
protocol[method] = {"supported": supported}
|
|
72
|
+
|
|
73
|
+
logger.info(f"core environment protocol for '{self.env_id}': " + " ".join(f"{m}={v['supported']}" for m, v in protocol.items()))
|
|
74
|
+
|
|
75
|
+
env = Env.get(self.env_id, deployed.env_version)
|
|
76
|
+
env.update_metadata({**env.metadata, VALIDATED_ENVIRONMENT_PROTOCOL_KEY: protocol})
|
|
77
|
+
context.metadata.setdefault("verifications", {})["env_core_protocol"] = protocol
|
|
78
|
+
return context
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _advertised_support(card: Optional[dict], method: str) -> Optional[bool]:
|
|
82
|
+
"""Per the backing card's advertised operations; None if unresolvable. An absent
|
|
83
|
+
key predates advertisement, when construction required the full data trio."""
|
|
84
|
+
if not card:
|
|
85
|
+
return None
|
|
86
|
+
children = card.get("children_environments") or []
|
|
87
|
+
backing = children[0] if len(children) == 1 else (card if not children else None)
|
|
88
|
+
if backing is None:
|
|
89
|
+
return None
|
|
90
|
+
advertised = (backing.get("capabilities") or {}).get("operations")
|
|
91
|
+
return True if advertised is None else method in advertised
|
|
92
|
+
|
|
93
|
+
async def _probe_operation(self, environment_url: str, method: str, params: dict) -> Optional[bool]:
|
|
94
|
+
"""Mutation-free: -32601 = unregistered; -32000 = gateway couldn't forward (None);
|
|
95
|
+
anything else — a result, or -32602 from the empty-parts add probe — proves registration."""
|
|
96
|
+
body = {"jsonrpc": "2.0", "id": f"verify-{method}", "method": method, "params": params}
|
|
97
|
+
try:
|
|
98
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
|
99
|
+
response = await client.post(f"{environment_url}{RPC_PATH}", json=body)
|
|
100
|
+
error = (response.json() or {}).get("error") or {}
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.warning(f"operation probe {method} for '{self.env_id}' failed: {e}")
|
|
103
|
+
return None
|
|
104
|
+
if error.get("code") == -32601:
|
|
105
|
+
return False
|
|
106
|
+
if error.get("code") == -32000:
|
|
107
|
+
return None
|
|
108
|
+
return True
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Install an A2A agent onto a running task container by executing the agent's install/v1 extension commands.
|
|
2
|
+
|
|
3
|
+
install_commands use `{name}` placeholders (Python str.format) for everything that
|
|
4
|
+
varies per install — container name, work dir, agent build context, A2A port, and
|
|
5
|
+
config-sourced creds. The runner resolves each declared `required_params` entry
|
|
6
|
+
via the registry below, `shlex.quote`s the value, and `cmd.format(**resolved)`s
|
|
7
|
+
each command before exec. No host env vars are exported.
|
|
8
|
+
|
|
9
|
+
To add a new param: register it in `_build_param_resolvers` (and document what
|
|
10
|
+
it returns); the agent's extension declares it in `required_params` to opt in.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import shlex
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from typing import Any, Callable, ClassVar, Optional
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from agent_env.task_step.context import DeployedAgent, TaskStepContext
|
|
25
|
+
from agent_env.entity_refs import EntityRef
|
|
26
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
INSTALL_COMMANDS_KEY = "install_commands"
|
|
31
|
+
INSTALL_COMMANDS_HOST_KEY = "install_commands_host"
|
|
32
|
+
REQUIRED_PARAMS_KEY = "required_params"
|
|
33
|
+
REQUIRED_PARAMS_HOST_KEY = "required_params_host"
|
|
34
|
+
A2A_PORT_KEY = "a2a_port"
|
|
35
|
+
|
|
36
|
+
def _build_param_resolvers(
|
|
37
|
+
*,
|
|
38
|
+
container_name: Optional[str],
|
|
39
|
+
work_dir: str,
|
|
40
|
+
agent_ctx_tar: str,
|
|
41
|
+
a2a_port: int,
|
|
42
|
+
config,
|
|
43
|
+
agent_name: str,
|
|
44
|
+
workspace_dir: Optional[str] = None,
|
|
45
|
+
litellm_api_key_override: Optional[str] = None,
|
|
46
|
+
) -> dict[str, Callable[[], str]]:
|
|
47
|
+
"""Per-install registry: required_params name -> zero-arg resolver returning the raw value.
|
|
48
|
+
|
|
49
|
+
Anything not in this registry is rejected at install time with a clear error,
|
|
50
|
+
so an extension can't ask for a param the runner doesn't know how to source.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def _resolve_workspace_dir() -> str:
|
|
54
|
+
# The agent's container working directory (e.g. a Harbor task's
|
|
55
|
+
# `workdir`). Opt-in: an agent declares `workspace_dir` in
|
|
56
|
+
# required_params and uses `{workspace_dir}` in its install_commands
|
|
57
|
+
# (e.g. `docker exec -w {workspace_dir} ...`). Inert for agents that
|
|
58
|
+
# don't, so existing agents are unaffected.
|
|
59
|
+
if workspace_dir is None:
|
|
60
|
+
raise KeyError(
|
|
61
|
+
"agent declares required_param 'workspace_dir' but the InstallAgentTaskStep "
|
|
62
|
+
"was constructed without one; pass workspace_dir=<container working directory>"
|
|
63
|
+
)
|
|
64
|
+
return workspace_dir
|
|
65
|
+
|
|
66
|
+
def _resolve_container() -> str:
|
|
67
|
+
if container_name is None:
|
|
68
|
+
raise KeyError(
|
|
69
|
+
"agent declares required_param 'container' but this is a host-mode install "
|
|
70
|
+
"(InstallAgentTaskStep has no container_name)"
|
|
71
|
+
)
|
|
72
|
+
return container_name
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
# Runner-supplied (from this step's own state)
|
|
76
|
+
"container": _resolve_container,
|
|
77
|
+
"work_dir": lambda: work_dir,
|
|
78
|
+
"agent_ctx_tar": lambda: agent_ctx_tar,
|
|
79
|
+
"a2a_port": lambda: str(a2a_port),
|
|
80
|
+
"agent_name": lambda: agent_name,
|
|
81
|
+
"workspace_dir": _resolve_workspace_dir,
|
|
82
|
+
# Config-sourced (from agent-env config)
|
|
83
|
+
"litellm_api_key": lambda: litellm_api_key_override or config.get_litellm_api_key(),
|
|
84
|
+
"litellm_base_url": lambda: config.get_litellm_base_url(),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class InstallAgentTaskStep(TaskStep):
|
|
89
|
+
type: ClassVar[str] = "install_agent"
|
|
90
|
+
entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
id: str,
|
|
95
|
+
version: Optional[int],
|
|
96
|
+
sandbox_name: str,
|
|
97
|
+
container_name: Optional[str] = None,
|
|
98
|
+
a2a_agent_id: str = "",
|
|
99
|
+
a2a_agent_version: Optional[int] = None,
|
|
100
|
+
agent_name: str = TaskStep.DEFAULT_AGENT_NAME,
|
|
101
|
+
a2a_port: Optional[int] = None,
|
|
102
|
+
workspace_dir: Optional[str] = None,
|
|
103
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
104
|
+
fail_task_on_error: bool = True,
|
|
105
|
+
):
|
|
106
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
107
|
+
if not a2a_agent_id:
|
|
108
|
+
raise ValueError("a2a_agent_id is required")
|
|
109
|
+
self.sandbox_name = sandbox_name
|
|
110
|
+
self.container_name = container_name
|
|
111
|
+
self.a2a_agent_id = a2a_agent_id
|
|
112
|
+
self.a2a_agent_version = a2a_agent_version
|
|
113
|
+
self.agent_name = agent_name
|
|
114
|
+
self.a2a_port = a2a_port
|
|
115
|
+
self.workspace_dir = workspace_dir
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict:
|
|
118
|
+
base = super().to_dict()
|
|
119
|
+
base["sandbox_name"] = self.sandbox_name
|
|
120
|
+
base["container_name"] = self.container_name
|
|
121
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
122
|
+
base["a2a_agent_version"] = self.a2a_agent_version
|
|
123
|
+
base["agent_name"] = self.agent_name
|
|
124
|
+
base["a2a_port"] = self.a2a_port
|
|
125
|
+
base["workspace_dir"] = self.workspace_dir
|
|
126
|
+
return base
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_dict(cls, data: dict) -> InstallAgentTaskStep:
|
|
130
|
+
return cls(
|
|
131
|
+
**cls._base_from_dict(data),
|
|
132
|
+
sandbox_name=data["sandbox_name"],
|
|
133
|
+
container_name=data.get("container_name"),
|
|
134
|
+
a2a_agent_id=data["a2a_agent_id"],
|
|
135
|
+
a2a_agent_version=data.get("a2a_agent_version"),
|
|
136
|
+
agent_name=data.get("agent_name", TaskStep.DEFAULT_AGENT_NAME),
|
|
137
|
+
a2a_port=data.get("a2a_port"),
|
|
138
|
+
workspace_dir=data.get("workspace_dir"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
142
|
+
from agent_env.a2a_agent import A2AAgent
|
|
143
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import (
|
|
144
|
+
SANDBOX_MODE_VM,
|
|
145
|
+
build_sandbox_provider,
|
|
146
|
+
get_sandbox_provider,
|
|
147
|
+
)
|
|
148
|
+
from agent_env.config import ConfigError, get_config
|
|
149
|
+
|
|
150
|
+
existing = next((a for a in context.deployed_agents if a.agent_name == self.agent_name), None)
|
|
151
|
+
if existing is not None:
|
|
152
|
+
raise RuntimeError(f"Agent with name '{self.agent_name}' is already deployed")
|
|
153
|
+
|
|
154
|
+
if self.container_name is not None:
|
|
155
|
+
containers = context.metadata.get("deployed_docker_containers", [])
|
|
156
|
+
container = next(
|
|
157
|
+
(
|
|
158
|
+
c for c in containers
|
|
159
|
+
if c.get("container_name") == self.container_name
|
|
160
|
+
and c.get("sandbox_name") == self.sandbox_name
|
|
161
|
+
),
|
|
162
|
+
None,
|
|
163
|
+
)
|
|
164
|
+
if container is None:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"Container '{self.container_name}' not found on sandbox '{self.sandbox_name}' in "
|
|
167
|
+
f"context.metadata['deployed_docker_containers']. Run RunDockerContainerTaskStep first."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
ds = next((s for s in context.deployed_sandboxes if s.sandbox_name == self.sandbox_name), None)
|
|
171
|
+
if ds is None:
|
|
172
|
+
raise RuntimeError(f"Sandbox '{self.sandbox_name}' not found in context.deployed_sandboxes")
|
|
173
|
+
if ds.sandbox_mode != SANDBOX_MODE_VM:
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
f"InstallAgent requires a VM-mode sandbox; '{self.sandbox_name}' is mode={ds.sandbox_mode!r}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
provider = (
|
|
179
|
+
build_sandbox_provider(ds.sandbox_type) if ds.sandbox_type else get_sandbox_provider()
|
|
180
|
+
)
|
|
181
|
+
sandbox = await provider.get_sandbox(ds.sandbox_id)
|
|
182
|
+
|
|
183
|
+
# Honor `--a2a-agent-id` override (same pattern as DeployAgentTaskStep) — lets the
|
|
184
|
+
# caller swap which agent gets installed without rewriting the task definition.
|
|
185
|
+
user_overrides = context.metadata.get("user_overrides", {})
|
|
186
|
+
a2a_agent_id = user_overrides.get("a2a_agent_id") or self.a2a_agent_id
|
|
187
|
+
agent = A2AAgent.get(a2a_agent_id, self.a2a_agent_version)
|
|
188
|
+
if not agent.docker_image_artifact.build_context_object_url:
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
f"Agent {agent.id} v{agent.version} has no build_context_object_url on its docker_image_artifact; "
|
|
191
|
+
f"install/v1 needs the build context (agent source files) to install into the task container"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
logger.info(f"Loading agent image {agent.docker_image_artifact.image_name} onto VM (needed to read agent card)...")
|
|
195
|
+
await sandbox.load_docker_images([agent.docker_image_artifact])
|
|
196
|
+
|
|
197
|
+
install_ext = await self._extract_install_extension(sandbox, agent.docker_image_artifact.image_name)
|
|
198
|
+
params = install_ext.get("params") or {}
|
|
199
|
+
host_mode = self.container_name is None
|
|
200
|
+
commands_key = INSTALL_COMMANDS_HOST_KEY if host_mode else INSTALL_COMMANDS_KEY
|
|
201
|
+
install_commands = params.get(commands_key)
|
|
202
|
+
a2a_port = self.a2a_port or params.get(A2A_PORT_KEY)
|
|
203
|
+
required_params = params.get(REQUIRED_PARAMS_HOST_KEY if host_mode else REQUIRED_PARAMS_KEY) or []
|
|
204
|
+
if host_mode and not install_commands:
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
f"Agent {agent.id} v{agent.version} install/v1 params have no '{INSTALL_COMMANDS_HOST_KEY}'; "
|
|
207
|
+
f"this agent does not support host-mode install (pass container_name for container mode)"
|
|
208
|
+
)
|
|
209
|
+
if not isinstance(install_commands, list) or not install_commands:
|
|
210
|
+
raise RuntimeError(f"Agent {agent.id} v{agent.version} install/v1 params missing '{commands_key}' (list)")
|
|
211
|
+
if not isinstance(a2a_port, int):
|
|
212
|
+
raise RuntimeError(f"Agent {agent.id} v{agent.version} install/v1 params missing '{A2A_PORT_KEY}' (int) and the step has no a2a_port override")
|
|
213
|
+
if not isinstance(required_params, list):
|
|
214
|
+
raise RuntimeError(f"Agent {agent.id} v{agent.version} install/v1 params 'required_params' must be a list")
|
|
215
|
+
|
|
216
|
+
if not ds.tunnel_urls or str(a2a_port) not in ds.tunnel_urls:
|
|
217
|
+
raise RuntimeError(
|
|
218
|
+
f"Sandbox '{self.sandbox_name}' does not tunnel port {a2a_port} required by agent {agent.id}; "
|
|
219
|
+
f"include exposed_ports=[{a2a_port}] on DeploySandboxTaskStep"
|
|
220
|
+
+ ("" if host_mode else f" and ports=[{a2a_port}] on RunDockerContainerTaskStep")
|
|
221
|
+
)
|
|
222
|
+
a2a_url = ds.tunnel_urls[str(a2a_port)]
|
|
223
|
+
self._check_tunnel_collision(context, ds.sandbox_id, a2a_url, a2a_port)
|
|
224
|
+
|
|
225
|
+
# Stage the agent build context onto the VM. The install_commands docker-cp from this dir.
|
|
226
|
+
work_dir = f"/tmp/install-agent-{self.agent_name}"
|
|
227
|
+
agent_ctx_tar = f"{work_dir}/agent-ctx.tar.gz"
|
|
228
|
+
await sandbox.exec_script(f"rm -rf {shlex.quote(work_dir)} && mkdir -p {shlex.quote(work_dir)}")
|
|
229
|
+
logger.info(f"Downloading agent build context {agent.docker_image_artifact.build_context_object_url} -> {agent_ctx_tar}")
|
|
230
|
+
await sandbox.load_s3_file(agent.docker_image_artifact.build_context_object_url, agent_ctx_tar)
|
|
231
|
+
|
|
232
|
+
# Resolve each required_param via the registry, shlex.quote so str.format
|
|
233
|
+
# substitution produces shell-safe tokens.
|
|
234
|
+
resolvers = _build_param_resolvers(
|
|
235
|
+
container_name=self.container_name,
|
|
236
|
+
work_dir=work_dir,
|
|
237
|
+
agent_ctx_tar=agent_ctx_tar,
|
|
238
|
+
a2a_port=a2a_port,
|
|
239
|
+
config=get_config(),
|
|
240
|
+
workspace_dir=self.workspace_dir,
|
|
241
|
+
agent_name=self.agent_name,
|
|
242
|
+
litellm_api_key_override=user_overrides.get("litellm_api_key"),
|
|
243
|
+
)
|
|
244
|
+
resolved: dict[str, str] = {}
|
|
245
|
+
for name in required_params:
|
|
246
|
+
if name not in resolvers:
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
f"Agent {agent.id} declares required_params={name!r} but no resolver is registered "
|
|
249
|
+
f"in InstallAgentTaskStep._build_param_resolvers. Known params: {sorted(resolvers)}"
|
|
250
|
+
)
|
|
251
|
+
try:
|
|
252
|
+
raw = resolvers[name]()
|
|
253
|
+
except (KeyError, ConfigError) as e:
|
|
254
|
+
raise RuntimeError(f"Failed to resolve required_param {name!r}: {e}") from e
|
|
255
|
+
resolved[name] = shlex.quote(raw)
|
|
256
|
+
|
|
257
|
+
target = "the VM host" if host_mode else f"container '{self.container_name}'"
|
|
258
|
+
logger.info(
|
|
259
|
+
f"Running {len(install_commands)} install command(s) against {target} "
|
|
260
|
+
f"on sandbox {ds.sandbox_id} (params resolved: {sorted(resolved)})"
|
|
261
|
+
)
|
|
262
|
+
for i, cmd in enumerate(install_commands, 1):
|
|
263
|
+
try:
|
|
264
|
+
formatted = cmd.format(**resolved)
|
|
265
|
+
except KeyError as e:
|
|
266
|
+
raise RuntimeError(
|
|
267
|
+
f"{commands_key}[{i-1}] references undeclared placeholder {{{e.args[0]}}}; "
|
|
268
|
+
f"add it to the extension's required_params"
|
|
269
|
+
)
|
|
270
|
+
logger.info(f" [{i}/{len(install_commands)}] {formatted.splitlines()[0][:140]}")
|
|
271
|
+
await sandbox.exec_script(formatted)
|
|
272
|
+
|
|
273
|
+
agent_card = await self._wait_for_agent_card(a2a_url)
|
|
274
|
+
logger.info(f"Agent '{self.agent_name}' reachable at {a2a_url} (card name={agent_card.get('name')})")
|
|
275
|
+
|
|
276
|
+
deployed = self._register_instance(agent, a2a_url, ds, agent_card)
|
|
277
|
+
|
|
278
|
+
context.deployed_agents.append(DeployedAgent(
|
|
279
|
+
agent_name=self.agent_name,
|
|
280
|
+
api_url=a2a_url,
|
|
281
|
+
sandbox_id=ds.sandbox_id,
|
|
282
|
+
sandbox_type=ds.sandbox_type,
|
|
283
|
+
a2a_url=a2a_url,
|
|
284
|
+
a2a_card=agent_card,
|
|
285
|
+
instance_id=deployed.instance_id,
|
|
286
|
+
on_host=host_mode,
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
default_model = agent.metadata.get("default_model") or get_config().get_model_for_role("agent")
|
|
290
|
+
if default_model and not context.default_agent_model:
|
|
291
|
+
context.default_agent_model = default_model
|
|
292
|
+
logger.info(f"Set context.default_agent_model to '{default_model}'")
|
|
293
|
+
|
|
294
|
+
return context
|
|
295
|
+
|
|
296
|
+
def _register_instance(self, agent, a2a_url: str, ds, agent_card: dict):
|
|
297
|
+
"""Register the installed agent in the instance store, same as A2AAgent.deploy(),
|
|
298
|
+
so store-resolving consumers (AddSkillsTaskStep, the a2a-agent CLIs) can find it."""
|
|
299
|
+
from agent_env.a2a_agent.a2a_agent import DeployedA2AAgent
|
|
300
|
+
from agent_env.a2a_agent.store import get_a2a_agent_instance_store
|
|
301
|
+
|
|
302
|
+
deployed = get_a2a_agent_instance_store().create_instance(
|
|
303
|
+
DeployedA2AAgent(
|
|
304
|
+
agent_id=agent.id,
|
|
305
|
+
agent_version=agent.version,
|
|
306
|
+
a2a_url=a2a_url,
|
|
307
|
+
sandbox_id=ds.sandbox_id,
|
|
308
|
+
agent_card=agent_card,
|
|
309
|
+
sandbox_type=ds.sandbox_type,
|
|
310
|
+
),
|
|
311
|
+
ttl_seconds=self._instance_ttl_seconds(ds),
|
|
312
|
+
)
|
|
313
|
+
logger.info(f"Registered installed agent '{self.agent_name}' as instance {deployed.instance_id}")
|
|
314
|
+
return deployed
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _instance_ttl_seconds(ds) -> int:
|
|
318
|
+
# The installed agent lives exactly as long as its host sandbox, so mirror the
|
|
319
|
+
# sandbox expiry when the context record carries one.
|
|
320
|
+
if ds.expires_at_utc:
|
|
321
|
+
try:
|
|
322
|
+
expires = datetime.strptime(ds.expires_at_utc, "%Y-%m-%d %H:%M UTC").replace(tzinfo=timezone.utc)
|
|
323
|
+
return max(60, int((expires - datetime.now(timezone.utc)).total_seconds()))
|
|
324
|
+
except ValueError:
|
|
325
|
+
pass
|
|
326
|
+
return TaskStep.DEFAULT_TTL_SECONDS
|
|
327
|
+
|
|
328
|
+
async def _extract_install_extension(self, sandbox, agent_image_name: str) -> dict:
|
|
329
|
+
"""Run the agent image briefly with --entrypoint python3 to dump its AGENT_CARD and find the install/v1 extension."""
|
|
330
|
+
from agent_env.a2a_agent import A2AAgent
|
|
331
|
+
|
|
332
|
+
script = (
|
|
333
|
+
"import sys, json; sys.path.insert(0, '/app'); "
|
|
334
|
+
"from a2a_server import AGENT_CARD; "
|
|
335
|
+
"print(json.dumps(AGENT_CARD.model_dump()))"
|
|
336
|
+
)
|
|
337
|
+
exit_code, stdout, stderr = await sandbox.exec_with_output(
|
|
338
|
+
"sudo", "docker", "run", "--rm", "--entrypoint", "python3",
|
|
339
|
+
agent_image_name, "-c", script,
|
|
340
|
+
)
|
|
341
|
+
if exit_code != 0:
|
|
342
|
+
raise RuntimeError(f"Failed to extract AGENT_CARD from {agent_image_name}: {stderr or stdout}")
|
|
343
|
+
try:
|
|
344
|
+
card = json.loads(stdout)
|
|
345
|
+
except json.JSONDecodeError as e:
|
|
346
|
+
raise RuntimeError(f"Could not parse AGENT_CARD JSON from {agent_image_name}: {e}; stdout={stdout[:500]!r}")
|
|
347
|
+
install_ext = A2AAgent.find_extension(card, A2AAgent.EXT_INSTALL)
|
|
348
|
+
if install_ext is None:
|
|
349
|
+
raise RuntimeError(
|
|
350
|
+
f"Agent image {agent_image_name} does not declare {A2AAgent.EXT_INSTALL} on its card; "
|
|
351
|
+
f"cannot use InstallAgentTaskStep with this agent"
|
|
352
|
+
)
|
|
353
|
+
return install_ext
|
|
354
|
+
|
|
355
|
+
def _check_tunnel_collision(self, context: TaskStepContext, sandbox_id: Optional[str], a2a_url: str, a2a_port: int) -> None:
|
|
356
|
+
# same sandbox + same port => identical tunnel URL
|
|
357
|
+
for other in context.deployed_agents:
|
|
358
|
+
if other.sandbox_id == sandbox_id and other.api_url == a2a_url:
|
|
359
|
+
raise RuntimeError(f"Tunnel for port {a2a_port} ({a2a_url}) already serves deployed agent '{other.agent_name}' on sandbox {sandbox_id}; pass a distinct a2a_port on this step")
|
|
360
|
+
|
|
361
|
+
async def _wait_for_agent_card(self, a2a_url: str, timeout_s: int = 300, poll_s: int = 5) -> dict:
|
|
362
|
+
deadline = asyncio.get_event_loop().time() + timeout_s
|
|
363
|
+
last_err: Exception | None = None
|
|
364
|
+
async with httpx.AsyncClient() as client:
|
|
365
|
+
while asyncio.get_event_loop().time() < deadline:
|
|
366
|
+
try:
|
|
367
|
+
resp = await client.get(f"{a2a_url}/.well-known/agent.json", timeout=10)
|
|
368
|
+
if resp.status_code == 200:
|
|
369
|
+
return resp.json()
|
|
370
|
+
except Exception as e:
|
|
371
|
+
last_err = e
|
|
372
|
+
await asyncio.sleep(poll_s)
|
|
373
|
+
raise RuntimeError(
|
|
374
|
+
f"Agent card at {a2a_url}/.well-known/agent.json did not respond within {timeout_s}s "
|
|
375
|
+
f"(last error: {type(last_err).__name__}: {last_err})"
|
|
376
|
+
)
|