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,173 @@
|
|
|
1
|
+
"""Verify A2A agent actually emits LiteLLM attribution on outbound LLM calls.
|
|
2
|
+
|
|
3
|
+
Optional behavioral counterpart to the static
|
|
4
|
+
`VerifyA2ALitellmAttributionStep`. When a runtime advertises its private probe,
|
|
5
|
+
this step asks whether it actually forwards project_id/task_id to the LLM call.
|
|
6
|
+
The probe is not part of the general AgentEnv SDK contract.
|
|
7
|
+
|
|
8
|
+
How it works:
|
|
9
|
+
1. The validator plants unique probe values in `initial_context.metadata`
|
|
10
|
+
before any prompt fires.
|
|
11
|
+
2. The standard `prompt_agent` flow POSTs those values via
|
|
12
|
+
`/ext/agent-config` and sends a prompt — the agent processes it,
|
|
13
|
+
makes an outbound LLM call, and records the attribution it claims
|
|
14
|
+
to have emitted in module-level state.
|
|
15
|
+
3. This step queries the agent's `/ext/attribution-probe` extension to
|
|
16
|
+
read back that recorded state.
|
|
17
|
+
4. Passes when the recorded values match the planted probe values.
|
|
18
|
+
|
|
19
|
+
Catches:
|
|
20
|
+
- Bridge ignores project_id/task_id from agent-config
|
|
21
|
+
- Bridge sets the wrong env var / header names
|
|
22
|
+
- Agent advertises support but never wires it through
|
|
23
|
+
|
|
24
|
+
Does NOT catch:
|
|
25
|
+
- CLI eats the env var and never emits the actual HTTP header (probe
|
|
26
|
+
trusts the bridge's claim about what it sent)
|
|
27
|
+
- Proxy strips the header on receive
|
|
28
|
+
|
|
29
|
+
For full end-to-end coverage of the latter two, a spend-log probe would
|
|
30
|
+
be needed (requires proxy admin access).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
from typing import ClassVar, Optional
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
|
|
40
|
+
from agent_env.task_step.context import TaskStepContext
|
|
41
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
EXT_ATTRIBUTION_PROBE_URI = "urn:agentenv:attribution-probe/v1"
|
|
46
|
+
EXT_ATTRIBUTION_PROBE_PATH = "/ext/attribution-probe"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VerifyA2ALitellmAttributionRuntimeStep(TaskStep):
|
|
50
|
+
type: ClassVar[str] = "verify_a2a_litellm_attribution_runtime"
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
id: str,
|
|
55
|
+
version: Optional[int],
|
|
56
|
+
a2a_agent_id: str,
|
|
57
|
+
expected_project_id: str,
|
|
58
|
+
expected_task_id: str,
|
|
59
|
+
a2a_agent_version: Optional[int] = None,
|
|
60
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
61
|
+
fail_task_on_error: bool = True,
|
|
62
|
+
):
|
|
63
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
64
|
+
self.a2a_agent_id = a2a_agent_id
|
|
65
|
+
self.a2a_agent_version = a2a_agent_version
|
|
66
|
+
self.expected_project_id = expected_project_id
|
|
67
|
+
self.expected_task_id = expected_task_id
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
base = super().to_dict()
|
|
71
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
72
|
+
base["a2a_agent_version"] = self.a2a_agent_version
|
|
73
|
+
base["expected_project_id"] = self.expected_project_id
|
|
74
|
+
base["expected_task_id"] = self.expected_task_id
|
|
75
|
+
return base
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_dict(cls, data: dict) -> "VerifyA2ALitellmAttributionRuntimeStep":
|
|
79
|
+
return cls(
|
|
80
|
+
**cls._base_from_dict(data),
|
|
81
|
+
a2a_agent_id=data["a2a_agent_id"],
|
|
82
|
+
a2a_agent_version=data.get("a2a_agent_version"),
|
|
83
|
+
expected_project_id=data["expected_project_id"],
|
|
84
|
+
expected_task_id=data["expected_task_id"],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
88
|
+
from agent_env.a2a_agent import A2AAgent
|
|
89
|
+
|
|
90
|
+
deployed = next((a for a in context.deployed_agents), None)
|
|
91
|
+
if deployed is None:
|
|
92
|
+
raise RuntimeError("No deployed agent found in context")
|
|
93
|
+
|
|
94
|
+
agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
|
|
95
|
+
|
|
96
|
+
# This is a runtime-specific diagnostic, not a general SDK capability.
|
|
97
|
+
# Its absence is therefore not a conformance failure.
|
|
98
|
+
probe_ext = A2AAgent.find_extension(deployed.a2a_card or {}, EXT_ATTRIBUTION_PROBE_URI)
|
|
99
|
+
if probe_ext is None:
|
|
100
|
+
logger.info(
|
|
101
|
+
"Agent '%s' does not advertise the attribution-probe extension "
|
|
102
|
+
"(%s) — skipping optional runtime attribution verification.",
|
|
103
|
+
self.a2a_agent_id, EXT_ATTRIBUTION_PROBE_URI,
|
|
104
|
+
)
|
|
105
|
+
validated = {
|
|
106
|
+
"passed": True,
|
|
107
|
+
"skipped": True,
|
|
108
|
+
"extension_present": False,
|
|
109
|
+
"reason": "optional runtime attribution probe not advertised",
|
|
110
|
+
"expected_project_id": self.expected_project_id,
|
|
111
|
+
"expected_task_id": self.expected_task_id,
|
|
112
|
+
}
|
|
113
|
+
agent.update_metadata({**agent.metadata, "validated_litellm_attribution_runtime": validated})
|
|
114
|
+
context.metadata.setdefault("verifications", {})["a2a_litellm_attribution_runtime"] = validated
|
|
115
|
+
return context
|
|
116
|
+
|
|
117
|
+
endpoint = (probe_ext.get("params") or {}).get("endpoint", EXT_ATTRIBUTION_PROBE_PATH)
|
|
118
|
+
url = (deployed.a2a_url or deployed.api_url) + endpoint
|
|
119
|
+
logger.info("Querying attribution probe at %s", url)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
123
|
+
resp = await client.post(url, json={})
|
|
124
|
+
resp.raise_for_status()
|
|
125
|
+
probe_data = resp.json()
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning("attribution-probe request failed: %s", e)
|
|
128
|
+
validated = {
|
|
129
|
+
"passed": False,
|
|
130
|
+
"extension_present": True,
|
|
131
|
+
"reason": f"probe request failed: {e}",
|
|
132
|
+
"expected_project_id": self.expected_project_id,
|
|
133
|
+
"expected_task_id": self.expected_task_id,
|
|
134
|
+
}
|
|
135
|
+
agent.update_metadata({**agent.metadata, "validated_litellm_attribution_runtime": validated})
|
|
136
|
+
context.metadata.setdefault("verifications", {})["a2a_litellm_attribution_runtime"] = validated
|
|
137
|
+
return context
|
|
138
|
+
|
|
139
|
+
last_seen = probe_data.get("last_seen_attribution") or {}
|
|
140
|
+
observed_project_id = last_seen.get("project_id")
|
|
141
|
+
observed_task_id = last_seen.get("task_id")
|
|
142
|
+
|
|
143
|
+
project_matches = observed_project_id == self.expected_project_id
|
|
144
|
+
task_matches = observed_task_id == self.expected_task_id
|
|
145
|
+
passed = project_matches and task_matches
|
|
146
|
+
|
|
147
|
+
if passed:
|
|
148
|
+
logger.info(
|
|
149
|
+
"Runtime attribution verified for '%s': observed projectId=%s taskId=%s",
|
|
150
|
+
self.a2a_agent_id, observed_project_id, observed_task_id,
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
logger.warning(
|
|
154
|
+
"Runtime attribution mismatch for '%s': expected projectId=%s taskId=%s, "
|
|
155
|
+
"observed projectId=%s taskId=%s",
|
|
156
|
+
self.a2a_agent_id, self.expected_project_id, self.expected_task_id,
|
|
157
|
+
observed_project_id, observed_task_id,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
validated = {
|
|
161
|
+
"passed": passed,
|
|
162
|
+
"extension_present": True,
|
|
163
|
+
"expected_project_id": self.expected_project_id,
|
|
164
|
+
"expected_task_id": self.expected_task_id,
|
|
165
|
+
"observed_project_id": observed_project_id,
|
|
166
|
+
"observed_task_id": observed_task_id,
|
|
167
|
+
"project_matches": project_matches,
|
|
168
|
+
"task_matches": task_matches,
|
|
169
|
+
"last_seen_at_utc": probe_data.get("last_seen_at_utc"),
|
|
170
|
+
}
|
|
171
|
+
agent.update_metadata({**agent.metadata, "validated_litellm_attribution_runtime": validated})
|
|
172
|
+
context.metadata.setdefault("verifications", {})["a2a_litellm_attribution_runtime"] = validated
|
|
173
|
+
return context
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Verify which input modalities an A2A agent functionally supports.
|
|
2
|
+
|
|
3
|
+
Covers `text`, `image/{png,jpeg,gif}`, `audio/{wav,mpeg,mp4,ogg}`,
|
|
4
|
+
`application/pdf`, and `video/mp4`.
|
|
5
|
+
The companion `PromptAgentTaskStep` probes (configured in
|
|
6
|
+
`A2AAgent.validate()`) actually send the messages via A2A; this step purely
|
|
7
|
+
grades the resulting `PromptResponse`s.
|
|
8
|
+
|
|
9
|
+
Grading is a deterministic case-insensitive substring match against a known
|
|
10
|
+
ground-truth string per probe. The outcome for each modality is one of:
|
|
11
|
+
- `passed`: response contains the expected string.
|
|
12
|
+
- `no_ingestion_evidence`: response succeeded but didn't reference the asset.
|
|
13
|
+
- `protocol_reject`: response was appended with `error_type` (wrapper accepted
|
|
14
|
+
the message but the agent task ended in `failed`).
|
|
15
|
+
- `probe_step_did_not_run`: no matching response — the upstream
|
|
16
|
+
`PromptAgentTaskStep` crashed before `context.prompt_responses.append(...)`.
|
|
17
|
+
|
|
18
|
+
The gemini wrapper currently passes all 12 modality probes (text, image
|
|
19
|
+
{png,jpeg,gif}, audio {wav,mpeg,mp4,ogg}, PDF, video/mp4, plus image/png
|
|
20
|
+
delivered via `FileWithUri` over `s3://` and presigned `https://`). New
|
|
21
|
+
A2A wrappers (claude-code, codex, grok) inherit this same probe set and
|
|
22
|
+
will report which subset they actually ingest.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import base64
|
|
28
|
+
import functools
|
|
29
|
+
import logging
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import ClassVar, Optional
|
|
32
|
+
|
|
33
|
+
from agent_env.task_step.context import TaskStepContext
|
|
34
|
+
from agent_env.entity_refs import EntityRef
|
|
35
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@functools.lru_cache(maxsize=None)
|
|
44
|
+
def _b64(name: str) -> str:
|
|
45
|
+
"""Load a binary fixture and return its base64-encoded form.
|
|
46
|
+
|
|
47
|
+
Fixtures live as native binary files in `fixtures/` so they can be
|
|
48
|
+
inspected (e.g. `afplay clip.wav`, `qlmanage -p document.pdf`) and
|
|
49
|
+
replaced in-place without re-chunking base64 into Python source.
|
|
50
|
+
"""
|
|
51
|
+
return base64.b64encode((_FIXTURES_DIR / name).read_bytes()).decode("ascii")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# 8×8 solid red squares in three formats — unambiguously red so the probe's
|
|
55
|
+
# deterministic "red" substring match holds. See `fixtures/README.md`.
|
|
56
|
+
IMAGE_PROBE_PNG_B64 = _b64("red.png")
|
|
57
|
+
IMAGE_PROBE_JPEG_B64 = _b64("red.jpg")
|
|
58
|
+
IMAGE_PROBE_GIF_B64 = _b64("red.gif")
|
|
59
|
+
|
|
60
|
+
# Per-format spoken "Say the word: <X>" clips with distinct target words —
|
|
61
|
+
# WAV/banana, MP3/telephone, M4A/bicycle, OGG/rainbow. Distinct targets force
|
|
62
|
+
# the model to actually decode each clip. See `fixtures/README.md` for the
|
|
63
|
+
# encoding pipeline, voice/volume/padding rationale, and regen commands.
|
|
64
|
+
AUDIO_PROBE_WAV_B64 = _b64("clip.wav")
|
|
65
|
+
AUDIO_PROBE_MP3_B64 = _b64("clip.mp3")
|
|
66
|
+
AUDIO_PROBE_M4A_B64 = _b64("clip.m4a")
|
|
67
|
+
AUDIO_PROBE_OGG_B64 = _b64("clip.ogg")
|
|
68
|
+
|
|
69
|
+
TEXT_PROBE_PROMPT = "Reply with exactly the word PINEAPPLE and nothing else."
|
|
70
|
+
TEXT_PROBE_EXPECTED = "PINEAPPLE"
|
|
71
|
+
|
|
72
|
+
IMAGE_PROBE_PROMPT = (
|
|
73
|
+
"What is the dominant color of the image attached? Answer in one word."
|
|
74
|
+
)
|
|
75
|
+
IMAGE_PROBE_EXPECTED = "red"
|
|
76
|
+
|
|
77
|
+
AUDIO_PROBE_PROMPT = (
|
|
78
|
+
"An audio clip is attached. Follow the spoken instruction in it and "
|
|
79
|
+
"respond with only the requested word."
|
|
80
|
+
)
|
|
81
|
+
AUDIO_WAV_EXPECTED = "banana"
|
|
82
|
+
AUDIO_MP3_EXPECTED = "telephone"
|
|
83
|
+
AUDIO_M4A_EXPECTED = "bicycle"
|
|
84
|
+
AUDIO_OGG_EXPECTED = "rainbow"
|
|
85
|
+
|
|
86
|
+
# 1.1KB single-page PDF with "ELEPHANT" inside an instructional paragraph
|
|
87
|
+
# (bare word alone trips letter-truncation edge cases). See `fixtures/README.md`.
|
|
88
|
+
PDF_PROBE_PDF_B64 = _b64("document.pdf")
|
|
89
|
+
PDF_PROBE_PROMPT = (
|
|
90
|
+
"A PDF document is attached. Read it and reply with only the target "
|
|
91
|
+
"word it specifies."
|
|
92
|
+
)
|
|
93
|
+
PDF_PROBE_EXPECTED = "elephant"
|
|
94
|
+
|
|
95
|
+
# 8.7KB h.264 mp4 (3s @ 480×360, 15fps, no audio) showing "GIRAFFE" on a
|
|
96
|
+
# static frame with on-screen instructional context. Delivered via
|
|
97
|
+
# FileWithUri (s3://) at validator runtime. See `fixtures/README.md`.
|
|
98
|
+
VIDEO_PROBE_MP4_B64 = _b64("clip.mp4")
|
|
99
|
+
VIDEO_PROBE_PROMPT = (
|
|
100
|
+
"A short video clip is attached. Read the target word displayed in "
|
|
101
|
+
"the video frames and reply with only that word."
|
|
102
|
+
)
|
|
103
|
+
VIDEO_PROBE_EXPECTED = "giraffe"
|
|
104
|
+
|
|
105
|
+
TEXT_PROBE_PARTS: list[dict] = [{"kind": "text", "text": TEXT_PROBE_PROMPT}]
|
|
106
|
+
IMAGE_PROBE_PARTS: list[dict] = [
|
|
107
|
+
{"kind": "text", "text": IMAGE_PROBE_PROMPT},
|
|
108
|
+
{"kind": "file", "file": {
|
|
109
|
+
"bytes": IMAGE_PROBE_PNG_B64,
|
|
110
|
+
"mimeType": "image/png",
|
|
111
|
+
"name": "red.png",
|
|
112
|
+
}},
|
|
113
|
+
]
|
|
114
|
+
IMAGE_JPEG_PROBE_PARTS: list[dict] = [
|
|
115
|
+
{"kind": "text", "text": IMAGE_PROBE_PROMPT},
|
|
116
|
+
{"kind": "file", "file": {
|
|
117
|
+
"bytes": IMAGE_PROBE_JPEG_B64,
|
|
118
|
+
"mimeType": "image/jpeg",
|
|
119
|
+
"name": "red.jpg",
|
|
120
|
+
}},
|
|
121
|
+
]
|
|
122
|
+
IMAGE_GIF_PROBE_PARTS: list[dict] = [
|
|
123
|
+
{"kind": "text", "text": IMAGE_PROBE_PROMPT},
|
|
124
|
+
{"kind": "file", "file": {
|
|
125
|
+
"bytes": IMAGE_PROBE_GIF_B64,
|
|
126
|
+
"mimeType": "image/gif",
|
|
127
|
+
"name": "red.gif",
|
|
128
|
+
}},
|
|
129
|
+
]
|
|
130
|
+
AUDIO_WAV_PROBE_PARTS: list[dict] = [
|
|
131
|
+
{"kind": "text", "text": AUDIO_PROBE_PROMPT},
|
|
132
|
+
{"kind": "file", "file": {
|
|
133
|
+
"bytes": AUDIO_PROBE_WAV_B64,
|
|
134
|
+
"mimeType": "audio/wav",
|
|
135
|
+
"name": "clip.wav",
|
|
136
|
+
}},
|
|
137
|
+
]
|
|
138
|
+
AUDIO_MP3_PROBE_PARTS: list[dict] = [
|
|
139
|
+
{"kind": "text", "text": AUDIO_PROBE_PROMPT},
|
|
140
|
+
{"kind": "file", "file": {
|
|
141
|
+
"bytes": AUDIO_PROBE_MP3_B64,
|
|
142
|
+
"mimeType": "audio/mpeg",
|
|
143
|
+
"name": "clip.mp3",
|
|
144
|
+
}},
|
|
145
|
+
]
|
|
146
|
+
AUDIO_M4A_PROBE_PARTS: list[dict] = [
|
|
147
|
+
{"kind": "text", "text": AUDIO_PROBE_PROMPT},
|
|
148
|
+
{"kind": "file", "file": {
|
|
149
|
+
"bytes": AUDIO_PROBE_M4A_B64,
|
|
150
|
+
"mimeType": "audio/mp4",
|
|
151
|
+
"name": "clip.m4a",
|
|
152
|
+
}},
|
|
153
|
+
]
|
|
154
|
+
AUDIO_OGG_PROBE_PARTS: list[dict] = [
|
|
155
|
+
{"kind": "text", "text": AUDIO_PROBE_PROMPT},
|
|
156
|
+
{"kind": "file", "file": {
|
|
157
|
+
"bytes": AUDIO_PROBE_OGG_B64,
|
|
158
|
+
"mimeType": "audio/ogg",
|
|
159
|
+
"name": "clip.ogg",
|
|
160
|
+
}},
|
|
161
|
+
]
|
|
162
|
+
PDF_PROBE_PARTS: list[dict] = [
|
|
163
|
+
{"kind": "text", "text": PDF_PROBE_PROMPT},
|
|
164
|
+
{"kind": "file", "file": {
|
|
165
|
+
"bytes": PDF_PROBE_PDF_B64,
|
|
166
|
+
"mimeType": "application/pdf",
|
|
167
|
+
"name": "document.pdf",
|
|
168
|
+
}},
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class VerifyA2AModalitiesStep(TaskStep):
|
|
173
|
+
"""Grade modality probes by reading `context.prompt_responses`.
|
|
174
|
+
|
|
175
|
+
`probes` is a list of dicts, each `{"modality": str, "prompt_id": str,
|
|
176
|
+
"expected": str}`. Each `prompt_id` should match an upstream
|
|
177
|
+
`PromptAgentTaskStep`'s `prompt_id`.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
type: ClassVar[str] = "verify_a2a_modalities"
|
|
181
|
+
entity_refs = (EntityRef.agent("a2a_agent_id"),)
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
id: str,
|
|
186
|
+
version: Optional[int],
|
|
187
|
+
a2a_agent_id: str,
|
|
188
|
+
probes: list[dict],
|
|
189
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
190
|
+
fail_task_on_error: bool = True,
|
|
191
|
+
):
|
|
192
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
193
|
+
self.a2a_agent_id = a2a_agent_id
|
|
194
|
+
self.probes = probes
|
|
195
|
+
|
|
196
|
+
def to_dict(self) -> dict:
|
|
197
|
+
base = super().to_dict()
|
|
198
|
+
base["a2a_agent_id"] = self.a2a_agent_id
|
|
199
|
+
base["probes"] = self.probes
|
|
200
|
+
return base
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def from_dict(cls, data: dict) -> VerifyA2AModalitiesStep:
|
|
204
|
+
return cls(
|
|
205
|
+
**cls._base_from_dict(data),
|
|
206
|
+
a2a_agent_id=data["a2a_agent_id"],
|
|
207
|
+
probes=data["probes"],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
211
|
+
from agent_env.a2a_agent import A2AAgent
|
|
212
|
+
|
|
213
|
+
results: dict[str, dict] = {}
|
|
214
|
+
for probe in self.probes:
|
|
215
|
+
modality = probe["modality"]
|
|
216
|
+
prompt_id = probe["prompt_id"]
|
|
217
|
+
expected = probe["expected"]
|
|
218
|
+
response = next(
|
|
219
|
+
(r for r in context.prompt_responses if r.prompt_id == prompt_id),
|
|
220
|
+
None,
|
|
221
|
+
)
|
|
222
|
+
if response is None:
|
|
223
|
+
# Upstream PromptAgentTaskStep crashed before appending its response
|
|
224
|
+
# (e.g., wrapper returned HTTP 4xx on message/send).
|
|
225
|
+
results[modality] = {
|
|
226
|
+
"supported": False,
|
|
227
|
+
"reason": "probe_step_did_not_run",
|
|
228
|
+
}
|
|
229
|
+
elif response.error_type:
|
|
230
|
+
results[modality] = {
|
|
231
|
+
"supported": False,
|
|
232
|
+
"reason": "protocol_reject",
|
|
233
|
+
"error_type": response.error_type,
|
|
234
|
+
"probe_response": (response.response or "")[:200],
|
|
235
|
+
}
|
|
236
|
+
elif expected.lower() in (response.response or "").lower():
|
|
237
|
+
results[modality] = {
|
|
238
|
+
"supported": True,
|
|
239
|
+
"reason": "passed",
|
|
240
|
+
"probe_response": response.response[:200],
|
|
241
|
+
}
|
|
242
|
+
else:
|
|
243
|
+
results[modality] = {
|
|
244
|
+
"supported": False,
|
|
245
|
+
"reason": "no_ingestion_evidence",
|
|
246
|
+
"probe_response": (response.response or "")[:200],
|
|
247
|
+
}
|
|
248
|
+
logger.info(
|
|
249
|
+
f"Modality '{modality}' graded: supported={results[modality]['supported']} "
|
|
250
|
+
f"reason={results[modality]['reason']}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Echo what the AgentCard declared, for cross-reference with the
|
|
254
|
+
# actual probe results.
|
|
255
|
+
agent = A2AAgent.get(self.a2a_agent_id)
|
|
256
|
+
card = agent.metadata.get("agent_card") or {}
|
|
257
|
+
declared = {
|
|
258
|
+
"default_input_modes": card.get("defaultInputModes"),
|
|
259
|
+
"default_output_modes": card.get("defaultOutputModes"),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
agent.update_metadata({
|
|
263
|
+
**agent.metadata,
|
|
264
|
+
"validated_modalities": {"input": results, "declared": declared},
|
|
265
|
+
})
|
|
266
|
+
context.metadata.setdefault("verifications", {})["a2a_modalities"] = {
|
|
267
|
+
"input": results,
|
|
268
|
+
"declared": declared,
|
|
269
|
+
}
|
|
270
|
+
return context
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Verify the A2A peer-agents extension end-to-end."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import ClassVar, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from agent_env.a2a_agent import A2AAgent
|
|
10
|
+
from agent_env.task_step.context import TaskStepContext
|
|
11
|
+
from agent_env.entity_refs import EntityRef
|
|
12
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VerifyA2APeerAgentsStep(TaskStep):
|
|
18
|
+
"""Consolidate four checks of the peer-agents extension and stamp results on agent.metadata.
|
|
19
|
+
|
|
20
|
+
Assumes upstream steps have deployed the agent under test plus a peer-target
|
|
21
|
+
agent, peered them via PeerAgentsTaskStep, and prompted the agent under test
|
|
22
|
+
to use peer_send_message to relay an `expected_token` from the peer."""
|
|
23
|
+
|
|
24
|
+
type: ClassVar[str] = "verify_a2a_peer_agents"
|
|
25
|
+
entity_refs = (EntityRef.agent("a2a_agent_id", version_field="a2a_agent_version"),)
|
|
26
|
+
|
|
27
|
+
def __init__(self, id: str, version: Optional[int], a2a_agent_id: str, a2a_agent_version: Optional[int], peer_prompt_id: str, expected_token: str, depends_on: Optional[list[TaskStepDependency]] = None, fail_task_on_error: bool = True):
|
|
28
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
29
|
+
self.a2a_agent_id = a2a_agent_id
|
|
30
|
+
self.a2a_agent_version = a2a_agent_version
|
|
31
|
+
self.peer_prompt_id = peer_prompt_id
|
|
32
|
+
self.expected_token = expected_token
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict:
|
|
35
|
+
return {**super().to_dict(), "a2a_agent_id": self.a2a_agent_id, "a2a_agent_version": self.a2a_agent_version, "peer_prompt_id": self.peer_prompt_id, "expected_token": self.expected_token}
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, data: dict) -> "VerifyA2APeerAgentsStep":
|
|
39
|
+
return cls(**cls._base_from_dict(data), a2a_agent_id=data["a2a_agent_id"], a2a_agent_version=data.get("a2a_agent_version"), peer_prompt_id=data["peer_prompt_id"], expected_token=data["expected_token"])
|
|
40
|
+
|
|
41
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
42
|
+
deployed = next((a for a in context.deployed_agents if a.agent_name == TaskStep.DEFAULT_AGENT_NAME), None)
|
|
43
|
+
if deployed is None:
|
|
44
|
+
raise RuntimeError(f"VerifyA2APeerAgentsStep: no deployed agent named '{TaskStep.DEFAULT_AGENT_NAME}' in context")
|
|
45
|
+
a2a_url = deployed.a2a_url or deployed.api_url
|
|
46
|
+
if not a2a_url:
|
|
47
|
+
raise RuntimeError(f"VerifyA2APeerAgentsStep: deployed agent has no a2a_url")
|
|
48
|
+
|
|
49
|
+
extension = A2AAgent.find_extension(deployed.a2a_card or {}, A2AAgent.EXT_PEER_AGENTS)
|
|
50
|
+
extension_advertised = extension is not None
|
|
51
|
+
endpoint = (extension.get("params") or {}).get("endpoint", "/ext/peer-agents") if extension else "/ext/peer-agents"
|
|
52
|
+
post_ok, list_ok = await self._check_round_trip(a2a_url, endpoint)
|
|
53
|
+
sidecar_registered = await self._check_sidecar_registered(a2a_url)
|
|
54
|
+
peer_call_ok, response_excerpt = self._check_peer_call(context)
|
|
55
|
+
|
|
56
|
+
peer_entry = {
|
|
57
|
+
"supported": extension_advertised and post_ok and list_ok and sidecar_registered and peer_call_ok,
|
|
58
|
+
"extension_advertised": extension_advertised,
|
|
59
|
+
"methods": {"set": {"supported": post_ok}, "list": {"supported": list_ok}},
|
|
60
|
+
"sidecar_self_registered": sidecar_registered,
|
|
61
|
+
"peer_call_succeeded": peer_call_ok,
|
|
62
|
+
"peer_call_response_excerpt": response_excerpt,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
agent = A2AAgent.get(self.a2a_agent_id, self.a2a_agent_version)
|
|
66
|
+
validated_ext = agent.metadata.get("validated_a2a_extensions", {})
|
|
67
|
+
validated_ext[A2AAgent.EXT_PEER_AGENTS] = peer_entry
|
|
68
|
+
agent.update_metadata({**agent.metadata, "validated_a2a_extensions": validated_ext})
|
|
69
|
+
|
|
70
|
+
context.metadata.setdefault("verifications", {})["a2a_peer_agents"] = peer_entry
|
|
71
|
+
logger.info(f"peer-agents validation: {peer_entry}")
|
|
72
|
+
return context
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
async def _check_round_trip(a2a_url: str, endpoint: str) -> tuple[bool, bool]:
|
|
76
|
+
probe = {"peers": [{"name": "probe", "url": "http://example.invalid"}]}
|
|
77
|
+
post_ok = list_ok = False
|
|
78
|
+
async with httpx.AsyncClient() as client:
|
|
79
|
+
try:
|
|
80
|
+
post_resp = await client.post(f"{a2a_url}{endpoint}", json=probe, timeout=30)
|
|
81
|
+
post_ok = post_resp.status_code < 400
|
|
82
|
+
get_resp = await client.get(f"{a2a_url}{endpoint}", timeout=30)
|
|
83
|
+
list_ok = get_resp.status_code < 400 and any(p.get("name") == "probe" for p in (get_resp.json().get("peers") or []))
|
|
84
|
+
finally:
|
|
85
|
+
await client.post(f"{a2a_url}{endpoint}", json={"peers": []}, timeout=30)
|
|
86
|
+
return post_ok, list_ok
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
async def _check_sidecar_registered(a2a_url: str) -> bool:
|
|
90
|
+
async with httpx.AsyncClient() as client:
|
|
91
|
+
resp = await client.get(f"{a2a_url}/ext/mcp-config", timeout=30)
|
|
92
|
+
if resp.status_code >= 400:
|
|
93
|
+
return False
|
|
94
|
+
servers = (resp.json() or {}).get("mcp_servers") or {}
|
|
95
|
+
return any("127.0.0.1" in (info.get("url") or "") and (info.get("url") or "").endswith("/mcp") for info in servers.values())
|
|
96
|
+
|
|
97
|
+
def _check_peer_call(self, context: TaskStepContext) -> tuple[bool, str]:
|
|
98
|
+
prompt_resp = next((p for p in context.prompt_responses if p.prompt_id == self.peer_prompt_id), None)
|
|
99
|
+
if prompt_resp is None:
|
|
100
|
+
return False, ""
|
|
101
|
+
response = prompt_resp.response or ""
|
|
102
|
+
return self.expected_token in response, response[:200]
|