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,901 @@
|
|
|
1
|
+
"""A2A agent validator — orchestrates the full validation flow.
|
|
2
|
+
|
|
3
|
+
Extracted from `A2AAgent.validate()` so the data model (`A2AAgent`) and the
|
|
4
|
+
validation behavior live in separate modules.
|
|
5
|
+
|
|
6
|
+
Adding a new modality probe is one new `ModalityProbe` entry in
|
|
7
|
+
`MODALITY_PROBES`. The step ID, prompt ID, `PromptAgentTaskStep`
|
|
8
|
+
instantiation, and grading-list entry are all derived from that single
|
|
9
|
+
tuple — no need to coordinate three places.
|
|
10
|
+
|
|
11
|
+
Use as `await A2AAgentValidator.validate(agent)`. `A2AAgent.validate()` is
|
|
12
|
+
a thin wrapper that delegates here.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import base64
|
|
18
|
+
import logging
|
|
19
|
+
import uuid
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Callable, Optional, TYPE_CHECKING, Union
|
|
22
|
+
|
|
23
|
+
from agentenv_protocol.a2a_agent import ObjectChangelogApplyResponse
|
|
24
|
+
|
|
25
|
+
from agent_env.a2a_agent.object_transfer import (
|
|
26
|
+
TRANSFER_TIMEOUT_SECONDS,
|
|
27
|
+
changelog_apply_call,
|
|
28
|
+
check_changelog_applied,
|
|
29
|
+
invoke_transfer,
|
|
30
|
+
)
|
|
31
|
+
from agent_env.config import get_config
|
|
32
|
+
from agent_env.store.routing import refuse_local_derivation
|
|
33
|
+
from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_modalities import (
|
|
34
|
+
AUDIO_M4A_EXPECTED,
|
|
35
|
+
AUDIO_M4A_PROBE_PARTS,
|
|
36
|
+
AUDIO_MP3_EXPECTED,
|
|
37
|
+
AUDIO_MP3_PROBE_PARTS,
|
|
38
|
+
AUDIO_OGG_EXPECTED,
|
|
39
|
+
AUDIO_OGG_PROBE_PARTS,
|
|
40
|
+
AUDIO_WAV_EXPECTED,
|
|
41
|
+
AUDIO_WAV_PROBE_PARTS,
|
|
42
|
+
IMAGE_GIF_PROBE_PARTS,
|
|
43
|
+
IMAGE_JPEG_PROBE_PARTS,
|
|
44
|
+
IMAGE_PROBE_EXPECTED,
|
|
45
|
+
IMAGE_PROBE_PARTS,
|
|
46
|
+
IMAGE_PROBE_PNG_B64,
|
|
47
|
+
IMAGE_PROBE_PROMPT,
|
|
48
|
+
PDF_PROBE_EXPECTED,
|
|
49
|
+
PDF_PROBE_PARTS,
|
|
50
|
+
TEXT_PROBE_EXPECTED,
|
|
51
|
+
TEXT_PROBE_PARTS,
|
|
52
|
+
VIDEO_PROBE_EXPECTED,
|
|
53
|
+
VIDEO_PROBE_MP4_B64,
|
|
54
|
+
VIDEO_PROBE_PROMPT,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING:
|
|
58
|
+
from agent_env.a2a_agent.a2a_agent import A2AAgent
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class _UploadedFixtures:
|
|
65
|
+
"""Object-store-hosted probe fixtures created at validation runtime."""
|
|
66
|
+
skill_object_url: str
|
|
67
|
+
png_object_uri: str
|
|
68
|
+
png_signed_url: str
|
|
69
|
+
mp4_object_uri: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_PartsFactory = Callable[[_UploadedFixtures], list[dict]]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class ModalityProbe:
|
|
77
|
+
"""One modality probe in the validator flow.
|
|
78
|
+
|
|
79
|
+
`parts` is either a static list of A2A parts (for inline probes whose
|
|
80
|
+
bytes are known at module load) or a callable that takes the runtime
|
|
81
|
+
`_UploadedFixtures` and returns parts (for URI probes whose s3:// or
|
|
82
|
+
presigned https:// URIs only exist after upload).
|
|
83
|
+
|
|
84
|
+
Each `ModalityProbe` produces exactly one `PromptAgentTaskStep` and
|
|
85
|
+
one entry in `VerifyA2AModalitiesStep.probes`. Step ID, prompt ID,
|
|
86
|
+
and grading entry all derive from `prompt_id_suffix`.
|
|
87
|
+
"""
|
|
88
|
+
modality: str
|
|
89
|
+
prompt_id_suffix: str
|
|
90
|
+
parts: Union[list[dict], _PartsFactory]
|
|
91
|
+
expected: str
|
|
92
|
+
timeout_s: int = 120
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
MODALITY_PROBES: list[ModalityProbe] = [
|
|
96
|
+
ModalityProbe("text", "modality-text", TEXT_PROBE_PARTS, TEXT_PROBE_EXPECTED),
|
|
97
|
+
ModalityProbe("image/png", "modality-image", IMAGE_PROBE_PARTS, IMAGE_PROBE_EXPECTED),
|
|
98
|
+
ModalityProbe("image/jpeg", "modality-image-jpeg", IMAGE_JPEG_PROBE_PARTS, IMAGE_PROBE_EXPECTED),
|
|
99
|
+
ModalityProbe("image/gif", "modality-image-gif", IMAGE_GIF_PROBE_PARTS, IMAGE_PROBE_EXPECTED),
|
|
100
|
+
ModalityProbe("audio/wav", "modality-audio-wav", AUDIO_WAV_PROBE_PARTS, AUDIO_WAV_EXPECTED),
|
|
101
|
+
ModalityProbe("audio/mpeg", "modality-audio-mp3", AUDIO_MP3_PROBE_PARTS, AUDIO_MP3_EXPECTED),
|
|
102
|
+
ModalityProbe("audio/mp4", "modality-audio-m4a", AUDIO_M4A_PROBE_PARTS, AUDIO_M4A_EXPECTED),
|
|
103
|
+
ModalityProbe("audio/ogg", "modality-audio-ogg", AUDIO_OGG_PROBE_PARTS, AUDIO_OGG_EXPECTED),
|
|
104
|
+
ModalityProbe("application/pdf", "modality-pdf", PDF_PROBE_PARTS, PDF_PROBE_EXPECTED),
|
|
105
|
+
ModalityProbe(
|
|
106
|
+
"image/png+uri-s3", "modality-image-uri-s3",
|
|
107
|
+
lambda f: [
|
|
108
|
+
{"kind": "text", "text": IMAGE_PROBE_PROMPT},
|
|
109
|
+
{"kind": "file", "file": {"uri": f.png_object_uri, "mimeType": "image/png", "name": "red.png"}},
|
|
110
|
+
],
|
|
111
|
+
IMAGE_PROBE_EXPECTED,
|
|
112
|
+
),
|
|
113
|
+
ModalityProbe(
|
|
114
|
+
"image/png+uri-https", "modality-image-uri-https",
|
|
115
|
+
lambda f: [
|
|
116
|
+
{"kind": "text", "text": IMAGE_PROBE_PROMPT},
|
|
117
|
+
{"kind": "file", "file": {"uri": f.png_signed_url, "mimeType": "image/png", "name": "red.png"}},
|
|
118
|
+
],
|
|
119
|
+
IMAGE_PROBE_EXPECTED,
|
|
120
|
+
),
|
|
121
|
+
ModalityProbe(
|
|
122
|
+
"video/mp4", "modality-video",
|
|
123
|
+
lambda f: [
|
|
124
|
+
{"kind": "text", "text": VIDEO_PROBE_PROMPT},
|
|
125
|
+
{"kind": "file", "file": {"uri": f.mp4_object_uri, "mimeType": "video/mp4", "name": "clip.mp4"}},
|
|
126
|
+
],
|
|
127
|
+
VIDEO_PROBE_EXPECTED,
|
|
128
|
+
timeout_s=180,
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class A2AAgentValidator:
|
|
134
|
+
"""Static namespace for running the full A2A agent validation flow."""
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
async def validate(
|
|
138
|
+
agent: "A2AAgent",
|
|
139
|
+
on_progress: Optional[Callable[[str], None]] = None,
|
|
140
|
+
litellm_api_key: Optional[str] = None,
|
|
141
|
+
) -> dict:
|
|
142
|
+
"""Run the A2A agent validator task: deploy env + agent, verify card,
|
|
143
|
+
modality probes, MCP tools, skills, snapshot extension, etc.
|
|
144
|
+
|
|
145
|
+
Returns the merged `verifications` dict from the run's task context.
|
|
146
|
+
"""
|
|
147
|
+
refuse_local_derivation(agent.id, "agent", "validating")
|
|
148
|
+
from agent_env.task import Task
|
|
149
|
+
from agent_env.task_step import (
|
|
150
|
+
AddSkillsTaskStep,
|
|
151
|
+
DeployAgentTaskStep,
|
|
152
|
+
DeployEnvTaskStep,
|
|
153
|
+
PromptAgentTaskStep,
|
|
154
|
+
RubricsVerifierTaskStep,
|
|
155
|
+
)
|
|
156
|
+
from agent_env.task_step.context import TaskStepContext
|
|
157
|
+
from agent_env.task_step.task_steps.a2a_agent_validator import (
|
|
158
|
+
VerifyA2AAgentCardStep,
|
|
159
|
+
VerifyA2AAgentConfigIdentityStep,
|
|
160
|
+
VerifyA2AAgentMCPStep,
|
|
161
|
+
VerifyA2ALitellmAttributionRuntimeStep,
|
|
162
|
+
VerifyA2ALitellmAttributionStep,
|
|
163
|
+
VerifyA2AModalitiesStep,
|
|
164
|
+
VerifyA2APeerAgentsStep,
|
|
165
|
+
VerifyA2ARoleStep,
|
|
166
|
+
VerifyA2ASkillConfigStep,
|
|
167
|
+
VerifyA2ASnapshotStep,
|
|
168
|
+
VerifyA2ASystemPromptStep,
|
|
169
|
+
VerifyA2ATrajectoryStep,
|
|
170
|
+
VerifyCoreA2AProtocolStep,
|
|
171
|
+
)
|
|
172
|
+
from agent_env.task_step.task_steps.add_skills import Skill
|
|
173
|
+
from agent_env.task_step.task_steps.peer_agents import AgentPeers, PeerAgentsTaskStep
|
|
174
|
+
from agent_env.task_step.task_steps.snapshot_agent_state import SnapshotAgentStateTaskStep
|
|
175
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
176
|
+
from agent_env.a2a_agent.a2a_agent import A2AAgent
|
|
177
|
+
|
|
178
|
+
task_id = f"validate-a2a-{agent.id}-v{agent.version}"
|
|
179
|
+
|
|
180
|
+
# ── Upload object-store fixtures (skill + URI probe bytes) ───────────
|
|
181
|
+
skill_object_url = await asyncio.to_thread(
|
|
182
|
+
A2AAgentValidator._upload_skill_fixture,
|
|
183
|
+
agent,
|
|
184
|
+
name="validator-test-s3",
|
|
185
|
+
description=(
|
|
186
|
+
"Validator test skill with project codename. "
|
|
187
|
+
"Use when asked about the project codename."
|
|
188
|
+
),
|
|
189
|
+
body=(
|
|
190
|
+
"When asked for the project codename, respond with: "
|
|
191
|
+
"VALIDATOR-S3-99"
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
skill_bundle_probe_url = await asyncio.to_thread(
|
|
195
|
+
A2AAgentValidator._upload_skill_fixture,
|
|
196
|
+
agent,
|
|
197
|
+
name="validator-probe-bundle",
|
|
198
|
+
description="Validator probe for the portable skill bundle variant.",
|
|
199
|
+
body="Portable skill bundle validation fixture.",
|
|
200
|
+
)
|
|
201
|
+
skill_s3_probe_url = await asyncio.to_thread(
|
|
202
|
+
A2AAgentValidator._upload_skill_fixture,
|
|
203
|
+
agent,
|
|
204
|
+
name="validator-probe-s3",
|
|
205
|
+
description="Validator probe for the legacy S3 skill variant.",
|
|
206
|
+
body="Legacy S3 skill validation fixture.",
|
|
207
|
+
)
|
|
208
|
+
fixtures = await asyncio.to_thread(A2AAgentValidator._upload_probe_fixtures, agent, skill_object_url)
|
|
209
|
+
|
|
210
|
+
# ── Build modality probe steps + matching grading list ───────────────
|
|
211
|
+
deploy_agent_id = f"{task_id}-deploy-agent"
|
|
212
|
+
modality_steps, grading_probes = A2AAgentValidator._build_modality_steps(task_id, fixtures, deploy_agent_id)
|
|
213
|
+
modality_step_ids = [s.id for s in modality_steps]
|
|
214
|
+
|
|
215
|
+
# ── Identifiers for non-modality steps ───────────────────────────────
|
|
216
|
+
protocol_prompt_id = f"{task_id}-protocol-prompt"
|
|
217
|
+
skill_prompt_id = f"{task_id}-skill-prompt"
|
|
218
|
+
skill_verifier_id = f"{task_id}-skill-rubric"
|
|
219
|
+
# LiteLLM attribution runtime probe: plant unique IDs on context.metadata
|
|
220
|
+
# so prompt_agent forwards them via /ext/agent-config; the agent records
|
|
221
|
+
# the attribution it emits on the resulting LLM call; the post-prompt
|
|
222
|
+
# verify step queries /ext/attribution-probe and asserts the planted
|
|
223
|
+
# values came through. Unique per validation run to avoid cross-run
|
|
224
|
+
# bleed when /ext/attribution-probe is stateful.
|
|
225
|
+
probe_project_id = f"probe-project-{uuid.uuid4().hex[:12]}"
|
|
226
|
+
probe_task_id = f"probe-task-{uuid.uuid4().hex[:12]}"
|
|
227
|
+
# Snapshot validation: plant a unique token on agent A, snapshot, deploy
|
|
228
|
+
# agent B with the snapshot loaded into a known target context_id, prompt
|
|
229
|
+
# agent B for recall, grade with a rubric.
|
|
230
|
+
snapshot_token = f"SNAPSHOT-TOKEN-{uuid.uuid4().hex[:12]}"
|
|
231
|
+
snapshot_plant_prompt_id = f"{task_id}-snapshot-plant-prompt"
|
|
232
|
+
snapshot_artifact_id = f"{task_id}-snapshot"
|
|
233
|
+
snapshot_target_agent_name = "snapshot-target-agent"
|
|
234
|
+
snapshot_target_context_id = uuid.uuid4().hex
|
|
235
|
+
snapshot_recall_prompt_id = f"{task_id}-snapshot-recall-prompt"
|
|
236
|
+
snapshot_recall_verifier_id = f"{task_id}-snapshot-recall-rubric"
|
|
237
|
+
snapshot_recall_criterion_id = "snapshot_recall_token"
|
|
238
|
+
# Peer-agents validation: deploy a second agent, peer it with the main one,
|
|
239
|
+
# have main relay a unique token from the peer, verify substring match.
|
|
240
|
+
peer_target_agent_name = "peer-target-agent"
|
|
241
|
+
peer_prompt_id = f"{task_id}-peer-prompt"
|
|
242
|
+
peer_token = f"PEERTOKEN-{uuid.uuid4().hex[:12]}"
|
|
243
|
+
|
|
244
|
+
deploy_agent_dep = [TaskStepDependency(task_step_id=deploy_agent_id)]
|
|
245
|
+
mcp_step_id = f"{task_id}-mcp"
|
|
246
|
+
skill_chain_id = f"{task_id}-add-skill"
|
|
247
|
+
snapshot_capture_id = f"{task_id}-snapshot-capture"
|
|
248
|
+
snapshot_target_deploy_id = f"{task_id}-deploy-snapshot-target"
|
|
249
|
+
snapshot_recall_rubric_id = f"{task_id}-snapshot-recall-rubric-step"
|
|
250
|
+
peer_target_deploy_id = f"{task_id}-deploy-peer-target"
|
|
251
|
+
peer_setup_id = f"{task_id}-peer-setup"
|
|
252
|
+
skill_rubric_id = f"{task_id}-skill-rubric-step"
|
|
253
|
+
# agent-changelog validation: a dedicated capture agent (enable_agent_changelog)
|
|
254
|
+
# writes a marker file via a tool call; the validator then drives apply
|
|
255
|
+
# onto a fresh agent (post-DAG) and checks the marker was reconstructed.
|
|
256
|
+
changelog_capture_agent_name = "changelog-capture-agent"
|
|
257
|
+
changelog_apply_agent_name = "changelog-apply-agent"
|
|
258
|
+
changelog_token = f"CHANGELOG-TOKEN-{uuid.uuid4().hex[:12]}"
|
|
259
|
+
changelog_marker_path = "/app/changelog_marker.txt"
|
|
260
|
+
changelog_capture_deploy_id = f"{task_id}-changelog-capture-deploy"
|
|
261
|
+
changelog_plant_prompt_id = f"{task_id}-changelog-plant"
|
|
262
|
+
changelog_apply_deploy_id = f"{task_id}-changelog-apply-deploy"
|
|
263
|
+
# install/v1 is validated post-DAG in _validate_install(), gated on the
|
|
264
|
+
# LIVE agent card (context.deployed_agents), so it never depends on a
|
|
265
|
+
# prior validation run having populated agent.metadata["agent_card"].
|
|
266
|
+
|
|
267
|
+
task = Task.put(id=task_id, steps=[
|
|
268
|
+
DeployEnvTaskStep(id=f"{task_id}-deploy-env", version=None, env_id=agent.VALIDATION_ENV_ID),
|
|
269
|
+
DeployAgentTaskStep(id=deploy_agent_id, version=None, env_ids=[agent.VALIDATION_ENV_ID], a2a_agent_id=agent.id, a2a_agent_version=agent.version),
|
|
270
|
+
VerifyA2AAgentCardStep(id=f"{task_id}-card", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
271
|
+
VerifyA2ALitellmAttributionStep(id=f"{task_id}-litellm-attribution", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
272
|
+
VerifyA2AAgentConfigIdentityStep(id=f"{task_id}-agent-config-identity", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
273
|
+
VerifyA2ARoleStep(id=f"{task_id}-role", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
274
|
+
VerifyA2ASystemPromptStep(id=f"{task_id}-system-prompt", version=None, a2a_agent_id=agent.id, a2a_agent_version=agent.version, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
275
|
+
PromptAgentTaskStep(id=protocol_prompt_id, version=None, prompt="Hello, respond with OK.", prompt_id=protocol_prompt_id, timeout_seconds=120, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
276
|
+
VerifyCoreA2AProtocolStep(id=f"{task_id}-protocol", version=None, a2a_agent_id=agent.id, prompt_id=protocol_prompt_id, depends_on=[TaskStepDependency(task_step_id=protocol_prompt_id)], fail_task_on_error=False),
|
|
277
|
+
VerifyA2ALitellmAttributionRuntimeStep(
|
|
278
|
+
id=f"{task_id}-litellm-attribution-runtime", version=None,
|
|
279
|
+
a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
280
|
+
expected_project_id=probe_project_id, expected_task_id=probe_task_id,
|
|
281
|
+
depends_on=[TaskStepDependency(task_step_id=protocol_prompt_id)],
|
|
282
|
+
fail_task_on_error=False,
|
|
283
|
+
),
|
|
284
|
+
*modality_steps,
|
|
285
|
+
VerifyA2AModalitiesStep(id=f"{task_id}-modalities", version=None, a2a_agent_id=agent.id, probes=grading_probes,
|
|
286
|
+
depends_on=[TaskStepDependency(task_step_id=sid) for sid in modality_step_ids], fail_task_on_error=False),
|
|
287
|
+
VerifyA2AAgentMCPStep(id=mcp_step_id, version=None, a2a_agent_id=agent.id, depends_on=deploy_agent_dep, fail_task_on_error=False),
|
|
288
|
+
VerifyA2ATrajectoryStep(id=f"{task_id}-trajectory", version=None, a2a_agent_id=agent.id, depends_on=[TaskStepDependency(task_step_id=mcp_step_id)], fail_task_on_error=False),
|
|
289
|
+
AddSkillsTaskStep(id=skill_chain_id, version=None, depends_on=deploy_agent_dep, fail_task_on_error=False, skills=[
|
|
290
|
+
Skill(name="validator-test", description="Validator test skill with secret code. Use when asked about the company secret code.", body="When asked for the company secret code, respond with: VALIDATOR-42"),
|
|
291
|
+
Skill(name="validator-test-s3", description="Validator test skill with project codename. Use when asked about the project codename.", s3_url=skill_object_url),
|
|
292
|
+
]),
|
|
293
|
+
PromptAgentTaskStep(id=skill_prompt_id, version=None, prompt="What is the company secret code? What is the project codename?", prompt_id=skill_prompt_id, timeout_seconds=120,
|
|
294
|
+
depends_on=[TaskStepDependency(task_step_id=skill_chain_id)], fail_task_on_error=False),
|
|
295
|
+
RubricsVerifierTaskStep(id=skill_rubric_id, version=None, prompt_id=skill_prompt_id, verifier_id=skill_verifier_id,
|
|
296
|
+
depends_on=[TaskStepDependency(task_step_id=skill_prompt_id)], fail_task_on_error=False,
|
|
297
|
+
criteria=[
|
|
298
|
+
{"id": "secret_code_inline", "description": "Response contains VALIDATOR-42"},
|
|
299
|
+
{"id": "secret_code_s3", "description": "Response contains VALIDATOR-S3-99"},
|
|
300
|
+
]),
|
|
301
|
+
VerifyA2ASkillConfigStep(
|
|
302
|
+
id=f"{task_id}-skill-config",
|
|
303
|
+
version=None,
|
|
304
|
+
a2a_agent_id=agent.id,
|
|
305
|
+
rubric_verifier_id=skill_verifier_id,
|
|
306
|
+
skill_bundle_object_url=skill_bundle_probe_url,
|
|
307
|
+
skill_s3_url=skill_s3_probe_url,
|
|
308
|
+
depends_on=[TaskStepDependency(task_step_id=skill_rubric_id)],
|
|
309
|
+
fail_task_on_error=False,
|
|
310
|
+
),
|
|
311
|
+
# ── snapshot extension validation ────────────────────────────────
|
|
312
|
+
PromptAgentTaskStep(
|
|
313
|
+
id=snapshot_plant_prompt_id, version=None,
|
|
314
|
+
prompt=f"Remember exactly this token: {snapshot_token}. Just acknowledge with OK.",
|
|
315
|
+
prompt_id=snapshot_plant_prompt_id, timeout_seconds=120,
|
|
316
|
+
depends_on=deploy_agent_dep, fail_task_on_error=False,
|
|
317
|
+
),
|
|
318
|
+
SnapshotAgentStateTaskStep(
|
|
319
|
+
id=snapshot_capture_id, version=None,
|
|
320
|
+
artifact_id=snapshot_artifact_id, prompt_id=snapshot_plant_prompt_id,
|
|
321
|
+
depends_on=[TaskStepDependency(task_step_id=snapshot_plant_prompt_id)],
|
|
322
|
+
fail_task_on_error=False,
|
|
323
|
+
),
|
|
324
|
+
DeployAgentTaskStep(
|
|
325
|
+
id=snapshot_target_deploy_id, version=None,
|
|
326
|
+
env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
327
|
+
agent_name=snapshot_target_agent_name,
|
|
328
|
+
agent_snapshot_files_artifact_id=snapshot_artifact_id,
|
|
329
|
+
agent_snapshot_target_context_id=snapshot_target_context_id,
|
|
330
|
+
depends_on=[TaskStepDependency(task_step_id=snapshot_capture_id)],
|
|
331
|
+
fail_task_on_error=False,
|
|
332
|
+
),
|
|
333
|
+
PromptAgentTaskStep(
|
|
334
|
+
id=snapshot_recall_prompt_id, version=None,
|
|
335
|
+
prompt="What was the exact token I told you to remember earlier? Output only the token, nothing else.",
|
|
336
|
+
prompt_id=snapshot_recall_prompt_id,
|
|
337
|
+
agent_name=snapshot_target_agent_name, context_id=snapshot_target_context_id,
|
|
338
|
+
timeout_seconds=120,
|
|
339
|
+
depends_on=[TaskStepDependency(task_step_id=snapshot_target_deploy_id)],
|
|
340
|
+
fail_task_on_error=False,
|
|
341
|
+
),
|
|
342
|
+
RubricsVerifierTaskStep(
|
|
343
|
+
id=snapshot_recall_rubric_id, version=None,
|
|
344
|
+
prompt_id=snapshot_recall_prompt_id, verifier_id=snapshot_recall_verifier_id,
|
|
345
|
+
depends_on=[TaskStepDependency(task_step_id=snapshot_recall_prompt_id)],
|
|
346
|
+
fail_task_on_error=False,
|
|
347
|
+
criteria=[{"id": snapshot_recall_criterion_id, "description": f"Response contains the exact token {snapshot_token}"}],
|
|
348
|
+
),
|
|
349
|
+
VerifyA2ASnapshotStep(
|
|
350
|
+
id=f"{task_id}-snapshot", version=None,
|
|
351
|
+
a2a_agent_id=agent.id,
|
|
352
|
+
rubric_verifier_id=snapshot_recall_verifier_id,
|
|
353
|
+
rubric_criterion_id=snapshot_recall_criterion_id,
|
|
354
|
+
depends_on=[TaskStepDependency(task_step_id=snapshot_recall_rubric_id)],
|
|
355
|
+
fail_task_on_error=False,
|
|
356
|
+
),
|
|
357
|
+
# ── peer-agents extension validation ─────────────────────────────
|
|
358
|
+
DeployAgentTaskStep(
|
|
359
|
+
id=peer_target_deploy_id, version=None,
|
|
360
|
+
env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
361
|
+
agent_name=peer_target_agent_name,
|
|
362
|
+
depends_on=deploy_agent_dep,
|
|
363
|
+
fail_task_on_error=False,
|
|
364
|
+
),
|
|
365
|
+
PeerAgentsTaskStep(
|
|
366
|
+
id=peer_setup_id, version=None,
|
|
367
|
+
peerings=[AgentPeers(source_agent_name=TaskStep.DEFAULT_AGENT_NAME, peer_agent_names=[peer_target_agent_name])],
|
|
368
|
+
depends_on=[TaskStepDependency(task_step_id=peer_target_deploy_id)],
|
|
369
|
+
fail_task_on_error=False,
|
|
370
|
+
),
|
|
371
|
+
PromptAgentTaskStep(
|
|
372
|
+
id=peer_prompt_id, version=None,
|
|
373
|
+
agent_name=TaskStep.DEFAULT_AGENT_NAME,
|
|
374
|
+
prompt_id=peer_prompt_id, timeout_seconds=300,
|
|
375
|
+
prompt=(
|
|
376
|
+
f"Use the peer_send_message MCP tool to send the peer named "
|
|
377
|
+
f"'{peer_target_agent_name}' the exact prompt: 'Reply with only "
|
|
378
|
+
f"the single token {peer_token} and nothing else.' Then report "
|
|
379
|
+
f"the peer's full reply verbatim."
|
|
380
|
+
),
|
|
381
|
+
depends_on=[TaskStepDependency(task_step_id=peer_setup_id)],
|
|
382
|
+
fail_task_on_error=False,
|
|
383
|
+
),
|
|
384
|
+
VerifyA2APeerAgentsStep(
|
|
385
|
+
id=f"{task_id}-peer-agents", version=None,
|
|
386
|
+
a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
387
|
+
peer_prompt_id=peer_prompt_id, expected_token=peer_token,
|
|
388
|
+
depends_on=[TaskStepDependency(task_step_id=peer_prompt_id)],
|
|
389
|
+
fail_task_on_error=False,
|
|
390
|
+
),
|
|
391
|
+
# ── agent-changelog validation (capture in DAG; apply driven
|
|
392
|
+
# by the validator post-run) ─────────────────────────────────────
|
|
393
|
+
DeployAgentTaskStep(
|
|
394
|
+
id=changelog_capture_deploy_id, version=None,
|
|
395
|
+
env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
396
|
+
agent_name=changelog_capture_agent_name, enable_agent_changelog=True,
|
|
397
|
+
depends_on=deploy_agent_dep, fail_task_on_error=False,
|
|
398
|
+
),
|
|
399
|
+
PromptAgentTaskStep(
|
|
400
|
+
id=changelog_plant_prompt_id, version=None,
|
|
401
|
+
agent_name=changelog_capture_agent_name,
|
|
402
|
+
prompt=(
|
|
403
|
+
f"Run exactly one bash command and nothing else: write the exact text "
|
|
404
|
+
f"'{changelog_token}' (no trailing newline) to the file {changelog_marker_path}, "
|
|
405
|
+
f"e.g. printf %s '{changelog_token}' > {changelog_marker_path}"
|
|
406
|
+
),
|
|
407
|
+
prompt_id=changelog_plant_prompt_id, timeout_seconds=180,
|
|
408
|
+
depends_on=[TaskStepDependency(task_step_id=changelog_capture_deploy_id)],
|
|
409
|
+
fail_task_on_error=False,
|
|
410
|
+
),
|
|
411
|
+
DeployAgentTaskStep(
|
|
412
|
+
id=changelog_apply_deploy_id, version=None,
|
|
413
|
+
env_ids=[], a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
414
|
+
agent_name=changelog_apply_agent_name,
|
|
415
|
+
depends_on=deploy_agent_dep, fail_task_on_error=False,
|
|
416
|
+
),
|
|
417
|
+
])
|
|
418
|
+
logger.info(f"Created A2A validation task: {task.id} version={task.version}")
|
|
419
|
+
|
|
420
|
+
# ── Execute task steps with progress callbacks ───────────────────────
|
|
421
|
+
log = on_progress or (lambda msg: None)
|
|
422
|
+
on_start = lambda i, total, step, ctx: log(f"Running step [{i+1}/{total}]: {step.type}...")
|
|
423
|
+
on_complete = lambda i, total, step, ctx, dur: log(f"Completed step [{i+1}/{total}]: {step.type} [{dur:.1f}s]")
|
|
424
|
+
initial_context = TaskStepContext()
|
|
425
|
+
if litellm_api_key:
|
|
426
|
+
initial_context.metadata["user_overrides"] = {"litellm_api_key": litellm_api_key}
|
|
427
|
+
# Plant LiteLLM attribution probe values so prompt_agent forwards them
|
|
428
|
+
# via /ext/agent-config — the runtime-attribution verify step reads them
|
|
429
|
+
# back from /ext/attribution-probe and asserts the agent's bridge
|
|
430
|
+
# actually wired them through to outbound LLM calls.
|
|
431
|
+
initial_context.metadata["project_id"] = probe_project_id
|
|
432
|
+
initial_context.metadata["task_id"] = probe_task_id
|
|
433
|
+
num_deploy_steps = 2
|
|
434
|
+
run_kwargs = {"on_step_start": on_start, "on_step_complete": on_complete}
|
|
435
|
+
|
|
436
|
+
# Phase 1: deploy steps (must succeed)
|
|
437
|
+
context = await task.run(start_step=0, end_step=num_deploy_steps, context=initial_context, **run_kwargs)
|
|
438
|
+
|
|
439
|
+
# Phase 2: validation DAG — every step opts into fail_task_on_error=False
|
|
440
|
+
# at construction so independent chains keep running on per-step failures.
|
|
441
|
+
context = await task.run(start_step=num_deploy_steps, context=context, **run_kwargs)
|
|
442
|
+
|
|
443
|
+
# agent-changelog: the validator drives apply directly — it discovers the
|
|
444
|
+
# captured prefix from context, applies it onto the fresh apply agent,
|
|
445
|
+
# and checks the marker file was reconstructed.
|
|
446
|
+
await A2AAgentValidator._validate_agent_changelog(
|
|
447
|
+
agent, context,
|
|
448
|
+
capture_agent_name=changelog_capture_agent_name,
|
|
449
|
+
apply_agent_name=changelog_apply_agent_name,
|
|
450
|
+
marker_path=changelog_marker_path, token=changelog_token,
|
|
451
|
+
on_progress=on_progress,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# install/v1: gated on the LIVE deployed card (not the stored one), so it
|
|
455
|
+
# runs identically regardless of whether this agent was validated before.
|
|
456
|
+
await A2AAgentValidator._validate_install(
|
|
457
|
+
agent, context, task_id=task_id, on_progress=on_progress,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
await A2AAgentValidator._cleanup_sandboxes(context)
|
|
461
|
+
return context.metadata.get("verifications", {})
|
|
462
|
+
|
|
463
|
+
@staticmethod
|
|
464
|
+
async def _validate_install(
|
|
465
|
+
agent: "A2AAgent",
|
|
466
|
+
context,
|
|
467
|
+
task_id: str,
|
|
468
|
+
on_progress: Optional[Callable[[str], None]] = None,
|
|
469
|
+
) -> None:
|
|
470
|
+
"""Validate the install/v1 extension, gated on the LIVE deployed card.
|
|
471
|
+
|
|
472
|
+
Runs after the main DAG (mirrors _validate_agent_changelog). The
|
|
473
|
+
install-support decision reads the freshly-deployed agent's card off
|
|
474
|
+
context.deployed_agents — NOT the stored agent.metadata["agent_card"] —
|
|
475
|
+
so the result is identical whether or not the agent was validated before
|
|
476
|
+
(fixes the --skip-validation first-run "upstream chain failure"). The
|
|
477
|
+
sub-chain runs on the MAIN context so its host sandbox is reaped by
|
|
478
|
+
_cleanup_sandboxes and its result lands in the returned verifications dict.
|
|
479
|
+
"""
|
|
480
|
+
from agent_env.a2a_agent import A2AAgent
|
|
481
|
+
from agent_env.task import Task
|
|
482
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
483
|
+
from agent_env.task_step.task_steps.a2a_agent_validator.verify_a2a_install import VerifyA2AInstallStep
|
|
484
|
+
from agent_env.task_step.task_steps.deploy_sandbox import DeploySandboxTaskStep
|
|
485
|
+
from agent_env.task_step.task_steps.install_agent import InstallAgentTaskStep
|
|
486
|
+
from agent_env.task_step.task_steps.prompt_agent import PromptAgentTaskStep
|
|
487
|
+
from agent_env.task_step.task_steps.run_docker_container import RunDockerContainerTaskStep
|
|
488
|
+
from agent_env.task_step.task_steps.verifiers.rubrics_verifier import RubricsVerifierTaskStep
|
|
489
|
+
|
|
490
|
+
log = on_progress or (lambda _msg: None)
|
|
491
|
+
|
|
492
|
+
def record(result: dict) -> None:
|
|
493
|
+
try:
|
|
494
|
+
fresh = A2AAgent.get(agent.id)
|
|
495
|
+
validated = dict(fresh.metadata.get("validated_a2a_extensions", {}))
|
|
496
|
+
validated[A2AAgent.EXT_INSTALL] = result
|
|
497
|
+
fresh.update_metadata({**fresh.metadata, "validated_a2a_extensions": validated})
|
|
498
|
+
except Exception as e: # noqa: BLE001
|
|
499
|
+
logger.warning(f"install/v1: failed to persist validated_a2a_extensions: {e}")
|
|
500
|
+
context.metadata.setdefault("verifications", {})["a2a_install"] = result
|
|
501
|
+
|
|
502
|
+
main = next((a for a in context.deployed_agents if a.agent_name == TaskStep.DEFAULT_AGENT_NAME), None)
|
|
503
|
+
# deploy_agent (Phase 1, fail_task_on_error=True) normally guarantees a
|
|
504
|
+
# fetched card, so a missing card here is an anomaly (e.g. a transient
|
|
505
|
+
# /.well-known/agent.json failure), NOT a genuine "not advertised". Log it
|
|
506
|
+
# so the failure mode is observable, and record a distinct reason rather
|
|
507
|
+
# than silently masquerading as absence of the extension.
|
|
508
|
+
card_unavailable = main is None or main.a2a_card is None
|
|
509
|
+
if card_unavailable:
|
|
510
|
+
logger.warning(
|
|
511
|
+
"install/v1: no live a2a_card for deployed agent %r (main_present=%s) — "
|
|
512
|
+
"possible transient card-fetch failure; recording install/v1 as unavailable",
|
|
513
|
+
TaskStep.DEFAULT_AGENT_NAME, main is not None,
|
|
514
|
+
)
|
|
515
|
+
live_card = (main.a2a_card if main else None) or {}
|
|
516
|
+
if A2AAgent.find_extension(live_card, A2AAgent.EXT_INSTALL) is None:
|
|
517
|
+
reason = (
|
|
518
|
+
"live agent card unavailable — deployed agent card was None"
|
|
519
|
+
if card_unavailable
|
|
520
|
+
else "install/v1 not advertised on live agent card"
|
|
521
|
+
)
|
|
522
|
+
log(f"install/v1 skipped: {reason}")
|
|
523
|
+
record({
|
|
524
|
+
"supported": False, "advertised": False,
|
|
525
|
+
"installed_successfully": False, "responded_to_prompt": False,
|
|
526
|
+
"skipped_reason": reason,
|
|
527
|
+
})
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
sandbox_name = "install-test-host"
|
|
531
|
+
container_name = "install-test-container"
|
|
532
|
+
install_agent_name = "install-test-agent"
|
|
533
|
+
prompt_id = f"{task_id}-install-test-prompt"
|
|
534
|
+
verifier_id = f"{task_id}-install-test-rubric"
|
|
535
|
+
deploy_sandbox_id = f"{task_id}-install-test-deploy-sandbox"
|
|
536
|
+
run_container_id = f"{task_id}-install-test-run-container"
|
|
537
|
+
install_id = f"{task_id}-install-test-install"
|
|
538
|
+
rubric_step_id = f"{task_id}-install-test-rubric-step"
|
|
539
|
+
validator_id = f"{task_id}-install-validator"
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
install_test_image = await asyncio.to_thread(A2AAgentValidator._upload_install_test_image_fixture, agent)
|
|
543
|
+
install_steps = [
|
|
544
|
+
DeploySandboxTaskStep(
|
|
545
|
+
id=deploy_sandbox_id, version=None,
|
|
546
|
+
sandbox_name=sandbox_name, sandbox_mode="vm",
|
|
547
|
+
cpu=2, memory_mb=4096, disk_size_gb=10,
|
|
548
|
+
exposed_ports=[8000], ttl_seconds=1800,
|
|
549
|
+
fail_task_on_error=False,
|
|
550
|
+
),
|
|
551
|
+
RunDockerContainerTaskStep(
|
|
552
|
+
id=run_container_id, version=None,
|
|
553
|
+
sandbox_name=sandbox_name,
|
|
554
|
+
docker_context_artifact_id=install_test_image.id,
|
|
555
|
+
docker_context_artifact_version=install_test_image.version,
|
|
556
|
+
container_name=container_name,
|
|
557
|
+
image_tag=f"{container_name}:latest",
|
|
558
|
+
ports=[8000],
|
|
559
|
+
command_override="sleep infinity",
|
|
560
|
+
depends_on=[TaskStepDependency(task_step_id=deploy_sandbox_id)],
|
|
561
|
+
fail_task_on_error=False,
|
|
562
|
+
),
|
|
563
|
+
InstallAgentTaskStep(
|
|
564
|
+
id=install_id, version=None,
|
|
565
|
+
sandbox_name=sandbox_name,
|
|
566
|
+
container_name=container_name,
|
|
567
|
+
a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
568
|
+
agent_name=install_agent_name,
|
|
569
|
+
depends_on=[TaskStepDependency(task_step_id=run_container_id)],
|
|
570
|
+
fail_task_on_error=False,
|
|
571
|
+
),
|
|
572
|
+
PromptAgentTaskStep(
|
|
573
|
+
id=prompt_id, version=None,
|
|
574
|
+
prompt="Respond with OK.",
|
|
575
|
+
prompt_id=prompt_id,
|
|
576
|
+
agent_name=install_agent_name,
|
|
577
|
+
timeout_seconds=120,
|
|
578
|
+
depends_on=[TaskStepDependency(task_step_id=install_id)],
|
|
579
|
+
fail_task_on_error=False,
|
|
580
|
+
),
|
|
581
|
+
RubricsVerifierTaskStep(
|
|
582
|
+
id=rubric_step_id, version=None,
|
|
583
|
+
prompt_id=prompt_id,
|
|
584
|
+
verifier_id=verifier_id,
|
|
585
|
+
# Trivial "response is non-empty" check — use the direct-LLM
|
|
586
|
+
# judge instead of auto-deploying an ephemeral judge agent.
|
|
587
|
+
use_agent_judge=False,
|
|
588
|
+
criteria=[
|
|
589
|
+
{"id": "responded", "description": "Response is non-empty (the installed agent actually responded)"},
|
|
590
|
+
],
|
|
591
|
+
depends_on=[TaskStepDependency(task_step_id=prompt_id)],
|
|
592
|
+
fail_task_on_error=False,
|
|
593
|
+
),
|
|
594
|
+
VerifyA2AInstallStep(
|
|
595
|
+
id=validator_id, version=None,
|
|
596
|
+
a2a_agent_id=agent.id, a2a_agent_version=agent.version,
|
|
597
|
+
agent_name=install_agent_name,
|
|
598
|
+
rubric_verifier_id=verifier_id,
|
|
599
|
+
depends_on=[TaskStepDependency(task_step_id=rubric_step_id)],
|
|
600
|
+
fail_task_on_error=False,
|
|
601
|
+
),
|
|
602
|
+
]
|
|
603
|
+
on_start = lambda i, total, step, ctx: log(f"Running install step [{i+1}/{total}]: {step.type}...")
|
|
604
|
+
on_complete = lambda i, total, step, ctx, dur: log(f"Completed install step [{i+1}/{total}]: {step.type} [{dur:.1f}s]")
|
|
605
|
+
sub_task = Task(id=f"{task_id}-install", version=None, steps=install_steps)
|
|
606
|
+
# Run on the MAIN context: the install host lands in context.deployed_agents
|
|
607
|
+
# (cleaned up by _cleanup_sandboxes) and VerifyA2AInstallStep writes the
|
|
608
|
+
# result into context.metadata["verifications"]["a2a_install"] itself.
|
|
609
|
+
await sub_task.run(context=context, on_step_start=on_start, on_step_complete=on_complete)
|
|
610
|
+
except Exception as e: # noqa: BLE001
|
|
611
|
+
logger.warning(f"install/v1 validation raised: {e}")
|
|
612
|
+
record({
|
|
613
|
+
"supported": False, "advertised": True,
|
|
614
|
+
"installed_successfully": False, "responded_to_prompt": False,
|
|
615
|
+
"error": f"install validation raised: {e}",
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
@staticmethod
|
|
619
|
+
async def _validate_agent_changelog(
|
|
620
|
+
agent: "A2AAgent",
|
|
621
|
+
context,
|
|
622
|
+
capture_agent_name: str,
|
|
623
|
+
apply_agent_name: str,
|
|
624
|
+
marker_path: str,
|
|
625
|
+
token: str,
|
|
626
|
+
on_progress: Optional[Callable[[str], None]] = None,
|
|
627
|
+
) -> None:
|
|
628
|
+
"""Drive the agent-changelog enable→apply round-trip directly (no task step).
|
|
629
|
+
|
|
630
|
+
Discovers the captured changelog prefix from context.metadata['agent_changelog']
|
|
631
|
+
(recorded by the capture agent's deploy), PUTs apply onto the fresh apply
|
|
632
|
+
agent, then deterministically checks the marker file was reconstructed by
|
|
633
|
+
exec-ing into the apply agent's sandbox. Records the outcome on the agent's
|
|
634
|
+
validated_a2a_extensions and in context.metadata['verifications']."""
|
|
635
|
+
from agent_env.a2a_agent import A2AAgent
|
|
636
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import (
|
|
637
|
+
SANDBOX_MODE_VM, build_sandbox_provider, get_agent_sandbox_provider,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
log = on_progress or (lambda _msg: None)
|
|
641
|
+
|
|
642
|
+
def record(*, supported, advertised, save_ok, apply_ok, roundtrip_ok, note=""):
|
|
643
|
+
try:
|
|
644
|
+
fresh = A2AAgent.get(agent.id)
|
|
645
|
+
validated = fresh.metadata.get("validated_a2a_extensions", {})
|
|
646
|
+
snap = validated.setdefault(A2AAgent.EXT_SNAPSHOT, {})
|
|
647
|
+
methods = snap.setdefault("methods", {})
|
|
648
|
+
methods[A2AAgent.SNAPSHOT_METHOD_ENABLE_CHANGELOG] = {"supported": save_ok}
|
|
649
|
+
methods[A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG] = {"supported": apply_ok and roundtrip_ok}
|
|
650
|
+
snap["changelog_roundtrip"] = roundtrip_ok
|
|
651
|
+
fresh.update_metadata({**fresh.metadata, "validated_a2a_extensions": validated})
|
|
652
|
+
except Exception as e: # noqa: BLE001
|
|
653
|
+
logger.warning(f"agent-changelog: failed to persist validated_a2a_extensions: {e}")
|
|
654
|
+
context.metadata.setdefault("verifications", {})["a2a_agent_changelog"] = {
|
|
655
|
+
"methods_advertised": advertised, "save": save_ok,
|
|
656
|
+
"apply": apply_ok, "roundtrip": roundtrip_ok, "note": note,
|
|
657
|
+
}
|
|
658
|
+
logger.info(
|
|
659
|
+
f"agent-changelog validation: advertised={advertised} save={save_ok} "
|
|
660
|
+
f"apply={apply_ok} roundtrip={roundtrip_ok} {note}"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
capture = next(
|
|
664
|
+
(e for e in (context.metadata.get("agent_changelog") or [])
|
|
665
|
+
if e.get("agent_name") == capture_agent_name), None)
|
|
666
|
+
capture_source = capture.get("object_url") if capture else None
|
|
667
|
+
apply_agent = next(
|
|
668
|
+
(d for d in context.deployed_agents if d.agent_name == apply_agent_name), None)
|
|
669
|
+
advertised = apply_agent is not None and A2AAgent.extension_method(
|
|
670
|
+
apply_agent.a2a_card or {}, A2AAgent.EXT_SNAPSHOT,
|
|
671
|
+
A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG) is not None
|
|
672
|
+
save_ok = capture_source is not None
|
|
673
|
+
if not save_ok or apply_agent is None or not advertised:
|
|
674
|
+
record(supported=False, advertised=advertised, save_ok=save_ok,
|
|
675
|
+
apply_ok=False, roundtrip_ok=False,
|
|
676
|
+
note="capture prefix or apply agent missing / extension not advertised")
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
# Apply the captured changelog onto the fresh apply agent.
|
|
680
|
+
method, path = A2AAgent.operation(
|
|
681
|
+
A2AAgent.find_extension(apply_agent.a2a_card, A2AAgent.EXT_SNAPSHOT),
|
|
682
|
+
A2AAgent.SNAPSHOT_METHOD_APPLY_CHANGELOG)
|
|
683
|
+
a2a_url = apply_agent.a2a_url or apply_agent.api_url
|
|
684
|
+
try:
|
|
685
|
+
call = await asyncio.to_thread(
|
|
686
|
+
changelog_apply_call,
|
|
687
|
+
method,
|
|
688
|
+
get_config().get_object_store(),
|
|
689
|
+
agent_name=apply_agent_name,
|
|
690
|
+
source_url=capture_source,
|
|
691
|
+
portable=capture.get("transfer_mode") == "objects",
|
|
692
|
+
)
|
|
693
|
+
except RuntimeError as e:
|
|
694
|
+
record(supported=False, advertised=advertised, save_ok=save_ok,
|
|
695
|
+
apply_ok=False, roundtrip_ok=False, note=str(e))
|
|
696
|
+
return
|
|
697
|
+
except Exception as e: # noqa: BLE001
|
|
698
|
+
record(supported=False, advertised=advertised, save_ok=save_ok,
|
|
699
|
+
apply_ok=False, roundtrip_ok=False, note=f"apply request failed: {e}")
|
|
700
|
+
return
|
|
701
|
+
try:
|
|
702
|
+
log("Applying agent-changelog onto apply agent...")
|
|
703
|
+
answer = await invoke_transfer(
|
|
704
|
+
a2a_url + path,
|
|
705
|
+
call,
|
|
706
|
+
verb="PUT",
|
|
707
|
+
operation="changelog apply",
|
|
708
|
+
timeout=TRANSFER_TIMEOUT_SECONDS,
|
|
709
|
+
response_model=ObjectChangelogApplyResponse,
|
|
710
|
+
)
|
|
711
|
+
if call.mode == "objects":
|
|
712
|
+
check_changelog_applied(answer, call, agent_name=apply_agent_name)
|
|
713
|
+
except Exception as e: # noqa: BLE001
|
|
714
|
+
record(supported=False, advertised=advertised, save_ok=save_ok,
|
|
715
|
+
apply_ok=False, roundtrip_ok=False, note=f"apply request failed: {e}")
|
|
716
|
+
return
|
|
717
|
+
|
|
718
|
+
# Deterministically verify the marker file was reconstructed.
|
|
719
|
+
roundtrip_ok = False
|
|
720
|
+
provider = None
|
|
721
|
+
try:
|
|
722
|
+
provider = (build_sandbox_provider(apply_agent.sandbox_type)
|
|
723
|
+
if apply_agent.sandbox_type else get_agent_sandbox_provider())
|
|
724
|
+
sandbox = await provider.get_sandbox(apply_agent.sandbox_id)
|
|
725
|
+
if sandbox.mode == SANDBOX_MODE_VM:
|
|
726
|
+
container = await A2AAgentValidator._discover_agent_container(sandbox)
|
|
727
|
+
args = ("sudo", "docker", "exec", container, "cat", marker_path)
|
|
728
|
+
else:
|
|
729
|
+
args = ("cat", marker_path)
|
|
730
|
+
exit_code, stdout, _ = await sandbox.exec_with_output(*args)
|
|
731
|
+
roundtrip_ok = exit_code == 0 and token in (stdout or "")
|
|
732
|
+
except Exception as e: # noqa: BLE001
|
|
733
|
+
record(supported=False, advertised=advertised, save_ok=save_ok,
|
|
734
|
+
apply_ok=True, roundtrip_ok=False, note=f"marker verification failed: {e}")
|
|
735
|
+
return
|
|
736
|
+
finally:
|
|
737
|
+
if provider is not None:
|
|
738
|
+
try:
|
|
739
|
+
await provider.close()
|
|
740
|
+
except Exception: # noqa: BLE001
|
|
741
|
+
pass
|
|
742
|
+
|
|
743
|
+
record(supported=roundtrip_ok, advertised=advertised, save_ok=save_ok,
|
|
744
|
+
apply_ok=True, roundtrip_ok=roundtrip_ok)
|
|
745
|
+
|
|
746
|
+
@staticmethod
|
|
747
|
+
async def _discover_agent_container(sandbox) -> str:
|
|
748
|
+
"""Find the agent container on a VM sandbox (mirrors collect_artifacts /
|
|
749
|
+
verify_sandbox): prefer 'agent-api', else the first 'a2a-agent-*'."""
|
|
750
|
+
exit_code, stdout, stderr = await sandbox.exec_with_output(
|
|
751
|
+
"sudo", "docker", "ps", "--format", "{{.Names}}")
|
|
752
|
+
if exit_code != 0:
|
|
753
|
+
raise RuntimeError(f"docker ps failed: {stderr[:200]}")
|
|
754
|
+
running = [n.strip() for n in stdout.splitlines() if n.strip()]
|
|
755
|
+
if sandbox.container_name in running:
|
|
756
|
+
return sandbox.container_name
|
|
757
|
+
fallback = [n for n in running if n.startswith("a2a-agent-")]
|
|
758
|
+
if not fallback:
|
|
759
|
+
raise RuntimeError(f"no agent container found; running: {running}")
|
|
760
|
+
return fallback[0]
|
|
761
|
+
|
|
762
|
+
@staticmethod
|
|
763
|
+
def _upload_install_test_image_fixture(agent: "A2AAgent"):
|
|
764
|
+
"""Upload a minimal Dockerfile as a FileArtifactUniverse so the install
|
|
765
|
+
validator chain has a base task image to layer the agent onto.
|
|
766
|
+
|
|
767
|
+
The Dockerfile is intentionally minimal: `FROM ubuntu:24.04`, no CMD.
|
|
768
|
+
The validator pairs it with `command_override="sleep infinity"` on
|
|
769
|
+
RunDockerContainerTaskStep so the bare container stays alive long
|
|
770
|
+
enough for the install to finish.
|
|
771
|
+
"""
|
|
772
|
+
from pathlib import Path
|
|
773
|
+
import os
|
|
774
|
+
import tempfile
|
|
775
|
+
import time
|
|
776
|
+
|
|
777
|
+
from agent_env.artifact.artifacts.file_artifact_universe import FileArtifactUniverse
|
|
778
|
+
|
|
779
|
+
ts = int(time.time())
|
|
780
|
+
universe_id = f"validate-install-image-{agent.id}-v{agent.version}-{ts}"
|
|
781
|
+
config = get_config()
|
|
782
|
+
s3_url = config.get_object_store().object_url(f"{config.get_artifact_key_prefix()}a2a_validator/install_test_image/{ts}/")
|
|
783
|
+
|
|
784
|
+
with tempfile.NamedTemporaryFile("w", suffix=".Dockerfile", delete=False) as f:
|
|
785
|
+
f.write("FROM ubuntu:24.04\n")
|
|
786
|
+
tmp_path = f.name
|
|
787
|
+
try:
|
|
788
|
+
universe = FileArtifactUniverse.put_bundled(
|
|
789
|
+
id=universe_id,
|
|
790
|
+
files={"Dockerfile": Path(tmp_path)},
|
|
791
|
+
s3_url=s3_url,
|
|
792
|
+
)
|
|
793
|
+
logger.info(f"Uploaded install-test image universe '{universe.id}' v{universe.version} to {s3_url}")
|
|
794
|
+
return universe
|
|
795
|
+
finally:
|
|
796
|
+
try:
|
|
797
|
+
os.unlink(tmp_path)
|
|
798
|
+
except OSError:
|
|
799
|
+
pass
|
|
800
|
+
|
|
801
|
+
@staticmethod
|
|
802
|
+
def _upload_skill_fixture(
|
|
803
|
+
agent: "A2AAgent", *, name: str, description: str, body: str
|
|
804
|
+
) -> str:
|
|
805
|
+
from agent_env.task_step.task_steps.add_skills import Skill
|
|
806
|
+
|
|
807
|
+
config = get_config()
|
|
808
|
+
store = config.get_object_store()
|
|
809
|
+
prefix = (
|
|
810
|
+
f"{config.get_artifact_key_prefix()}a2a_validator/validator_skill/{agent.id}-v{agent.version}/{name}/"
|
|
811
|
+
)
|
|
812
|
+
skill = Skill(
|
|
813
|
+
name=name,
|
|
814
|
+
description=description,
|
|
815
|
+
body=body,
|
|
816
|
+
)
|
|
817
|
+
store.put(f"{prefix}SKILL.md", skill.to_skill_md().encode(), content_type="text/markdown", allow_overwrite=True)
|
|
818
|
+
object_url = store.object_url(prefix)
|
|
819
|
+
logger.info(f"Uploaded test skill to {object_url}")
|
|
820
|
+
return object_url
|
|
821
|
+
|
|
822
|
+
@staticmethod
|
|
823
|
+
def _upload_probe_fixtures(agent: "A2AAgent", skill_object_url: str) -> _UploadedFixtures:
|
|
824
|
+
"""Upload `red.png` and `clip.mp4` for the URI probes.
|
|
825
|
+
|
|
826
|
+
The PNG bytes are also sent inline via the existing PNG probe — comparing
|
|
827
|
+
the two outcomes verifies both FilePart delivery modes work for this
|
|
828
|
+
wrapper. The MP4 is too big for practical inline use and the URI path is
|
|
829
|
+
the point.
|
|
830
|
+
"""
|
|
831
|
+
|
|
832
|
+
config = get_config()
|
|
833
|
+
store = config.get_object_store()
|
|
834
|
+
prefix = f"{config.get_artifact_key_prefix()}a2a_validator/probe_fixtures/{agent.id}-v{agent.version}/"
|
|
835
|
+
|
|
836
|
+
png_object_uri = store.put(f"{prefix}red.png", base64.b64decode(IMAGE_PROBE_PNG_B64), content_type="image/png", allow_overwrite=True)
|
|
837
|
+
png_signed_url = store.signed_get_url(png_object_uri)
|
|
838
|
+
if png_signed_url is None:
|
|
839
|
+
raise RuntimeError("A2A validation requires a signable object store for the presigned-URI probe.")
|
|
840
|
+
logger.info(f"Uploaded probe fixture to {png_object_uri}")
|
|
841
|
+
|
|
842
|
+
mp4_object_uri = store.put(f"{prefix}clip.mp4", base64.b64decode(VIDEO_PROBE_MP4_B64), content_type="video/mp4", allow_overwrite=True)
|
|
843
|
+
logger.info(f"Uploaded video probe fixture to {mp4_object_uri}")
|
|
844
|
+
|
|
845
|
+
return _UploadedFixtures(
|
|
846
|
+
skill_object_url=skill_object_url,
|
|
847
|
+
png_object_uri=png_object_uri,
|
|
848
|
+
png_signed_url=png_signed_url,
|
|
849
|
+
mp4_object_uri=mp4_object_uri,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
@staticmethod
|
|
853
|
+
def _build_modality_steps(task_id: str, fixtures: _UploadedFixtures, deploy_agent_id: str):
|
|
854
|
+
"""Build modality probe `PromptAgentTaskStep`s and the matching grading list.
|
|
855
|
+
|
|
856
|
+
All 11 prompts depend only on deploy-agent so they fan out in parallel
|
|
857
|
+
once the agent is deployed. Adding a new probe is one new entry in
|
|
858
|
+
MODALITY_PROBES, nothing else.
|
|
859
|
+
"""
|
|
860
|
+
from agent_env.task_step import PromptAgentTaskStep
|
|
861
|
+
from agent_env.task_step.task_step import TaskStepDependency
|
|
862
|
+
prompt_steps: list[PromptAgentTaskStep] = []
|
|
863
|
+
grading_probes: list[dict] = []
|
|
864
|
+
for probe in MODALITY_PROBES:
|
|
865
|
+
prompt_id = f"{task_id}-{probe.prompt_id_suffix}-prompt"
|
|
866
|
+
parts = probe.parts(fixtures) if callable(probe.parts) else probe.parts
|
|
867
|
+
prompt_steps.append(PromptAgentTaskStep(
|
|
868
|
+
id=f"{task_id}-{probe.prompt_id_suffix}-prompt-step",
|
|
869
|
+
version=None, parts=parts, prompt_id=prompt_id, timeout_seconds=probe.timeout_s,
|
|
870
|
+
depends_on=[TaskStepDependency(task_step_id=deploy_agent_id)],
|
|
871
|
+
fail_task_on_error=False,
|
|
872
|
+
))
|
|
873
|
+
grading_probes.append({
|
|
874
|
+
"modality": probe.modality,
|
|
875
|
+
"prompt_id": prompt_id,
|
|
876
|
+
"expected": probe.expected,
|
|
877
|
+
})
|
|
878
|
+
return prompt_steps, grading_probes
|
|
879
|
+
|
|
880
|
+
@staticmethod
|
|
881
|
+
async def _cleanup_sandboxes(context) -> None:
|
|
882
|
+
from agent_env.env.env import DeployedSandboxEnv
|
|
883
|
+
from agent_env.providers import build_sandbox_provider, get_agent_sandbox_provider, get_env_sandbox_provider
|
|
884
|
+
for deployed_env in context.deployed_envs:
|
|
885
|
+
if not isinstance(deployed_env, DeployedSandboxEnv): # an env outside our sandboxes owns its own lifetime
|
|
886
|
+
continue
|
|
887
|
+
try:
|
|
888
|
+
provider = build_sandbox_provider(deployed_env.sandbox_type) if deployed_env.sandbox_type else get_env_sandbox_provider()
|
|
889
|
+
sandbox = await provider.get_sandbox(deployed_env.sandbox_id)
|
|
890
|
+
await sandbox.terminate()
|
|
891
|
+
except Exception as e:
|
|
892
|
+
logger.warning(f"Failed to clean up env sandbox {deployed_env.sandbox_id}: {e}")
|
|
893
|
+
for deployed_agent in context.deployed_agents:
|
|
894
|
+
if not deployed_agent.sandbox_id:
|
|
895
|
+
continue
|
|
896
|
+
try:
|
|
897
|
+
provider = build_sandbox_provider(deployed_agent.sandbox_type) if deployed_agent.sandbox_type else get_agent_sandbox_provider()
|
|
898
|
+
sandbox = await provider.get_sandbox(deployed_agent.sandbox_id)
|
|
899
|
+
await sandbox.terminate()
|
|
900
|
+
except Exception as e:
|
|
901
|
+
logger.warning(f"Failed to clean up agent sandbox {deployed_agent.sandbox_id}: {e}")
|