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,241 @@
|
|
|
1
|
+
"""Verify universe load/export roundtrip for MultiEnv compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
|
11
|
+
|
|
12
|
+
from agent_env.env.env_artifact_store import EnvArtifactType, get_env_artifact_store
|
|
13
|
+
from agent_env.artifact.store import artifact_write_lock
|
|
14
|
+
from agent_env.task_step.context import TaskStepContext
|
|
15
|
+
from agent_env.entity_refs import EntityRef
|
|
16
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
17
|
+
|
|
18
|
+
from .universe_comparison import classify_issues, compare_dicts, normalize
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from agent_env.env.env import DeployedEnv
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VerifyUniverseLoadExportRoundtripStep(TaskStep):
|
|
27
|
+
type: ClassVar[str] = "verify_universe_load_export_roundtrip"
|
|
28
|
+
entity_refs = (
|
|
29
|
+
EntityRef.env("env_id"),
|
|
30
|
+
EntityRef.artifact("universe_artifact_id", version_field="universe_artifact_version", artifact_type="environment_universe"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
id: str,
|
|
36
|
+
version: Optional[int],
|
|
37
|
+
env_id: str,
|
|
38
|
+
universe_artifact_id: str,
|
|
39
|
+
universe_artifact_version: Optional[int] = None,
|
|
40
|
+
emit_file_artifact_universe: bool = False,
|
|
41
|
+
persist_result: bool = True,
|
|
42
|
+
depends_on: Optional[list[TaskStepDependency]] = None,
|
|
43
|
+
fail_task_on_error: bool = True,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
46
|
+
self.env_id = env_id
|
|
47
|
+
self.universe_artifact_id = universe_artifact_id
|
|
48
|
+
self.universe_artifact_version = universe_artifact_version
|
|
49
|
+
# When set, also bundle the original/export1/export2 JSON into a FileArtifactUniverse so a
|
|
50
|
+
# downstream agent-judge can diff them on its filesystem. Off by default — the
|
|
51
|
+
# programmatic verdict does not need it. The bundle's id is deterministic (see
|
|
52
|
+
# file_artifact_universe_id) so the callsite can reference it statically.
|
|
53
|
+
self.emit_file_artifact_universe = emit_file_artifact_universe
|
|
54
|
+
# When False, compute the programmatic verdict into context but DON'T persist it to the
|
|
55
|
+
# env-artifact store — a downstream step (CombineUniverseVerdictsStep) becomes the single
|
|
56
|
+
# authoritative writer, so a crash before the judge merges can't leave an ungated half-write
|
|
57
|
+
# masquerading as final. Defaults True to preserve the step's standalone contract.
|
|
58
|
+
self.persist_result = persist_result
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
base = super().to_dict()
|
|
62
|
+
base["env_id"] = self.env_id
|
|
63
|
+
base["universe_artifact_id"] = self.universe_artifact_id
|
|
64
|
+
base["universe_artifact_version"] = self.universe_artifact_version
|
|
65
|
+
base["emit_file_artifact_universe"] = self.emit_file_artifact_universe
|
|
66
|
+
base["persist_result"] = self.persist_result
|
|
67
|
+
return base
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_dict(cls, data: dict) -> VerifyUniverseLoadExportRoundtripStep:
|
|
71
|
+
return cls(
|
|
72
|
+
**cls._base_from_dict(data),
|
|
73
|
+
env_id=data["env_id"],
|
|
74
|
+
universe_artifact_id=data["universe_artifact_id"],
|
|
75
|
+
universe_artifact_version=data.get("universe_artifact_version"),
|
|
76
|
+
emit_file_artifact_universe=data.get("emit_file_artifact_universe", False),
|
|
77
|
+
persist_result=data.get("persist_result", True),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def file_artifact_universe_id(env_id: str, env_version: int, universe_id: str, universe_version: int) -> str:
|
|
82
|
+
"""Deterministic id of the emitted FileArtifactUniverse, so the callsite can reference it
|
|
83
|
+
statically without a runtime context lookup (mirrors the export1 artifact id scheme)."""
|
|
84
|
+
return f"validate-{env_id}-v{env_version}-{universe_id}-v{universe_version}-fau"
|
|
85
|
+
|
|
86
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
87
|
+
from agent_env.artifact import EnvironmentUniverseArtifact
|
|
88
|
+
from agent_env.env.env import Env
|
|
89
|
+
|
|
90
|
+
# Phase 1: Resolve deployed env and universe artifact
|
|
91
|
+
deployed = next((d for d in context.deployed_envs if d.env_id == self.env_id), None)
|
|
92
|
+
if deployed is None:
|
|
93
|
+
raise RuntimeError(f"Env '{self.env_id}' not found in context.deployed_envs")
|
|
94
|
+
env = Env.get(deployed.env_id, deployed.env_version)
|
|
95
|
+
env = await type(env).from_deployed_env(deployed)
|
|
96
|
+
universe = EnvironmentUniverseArtifact.get(self.universe_artifact_id, self.universe_artifact_version)
|
|
97
|
+
environment_artifacts = universe.get_environment_artifacts()
|
|
98
|
+
environment_names = [sa.environment_name for sa in environment_artifacts]
|
|
99
|
+
|
|
100
|
+
# Phase 2: Load original universe + read original data for comparison
|
|
101
|
+
logger.info(f"Loading universe {self.universe_artifact_id} into env {self.env_id}...")
|
|
102
|
+
await env.load_environment_universe_artifact(universe)
|
|
103
|
+
originals = {}
|
|
104
|
+
for sa in environment_artifacts:
|
|
105
|
+
try:
|
|
106
|
+
data = await asyncio.to_thread(lambda sa=sa: sa.get_file_artifact().load())
|
|
107
|
+
originals[sa.environment_name] = normalize(json.loads(data))
|
|
108
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
109
|
+
logger.info(f"{sa.environment_name}: non-JSON artifact, will only compare export1 vs export2")
|
|
110
|
+
|
|
111
|
+
# Phase 3: Export #1
|
|
112
|
+
logger.info("Exporting state (round 1)...")
|
|
113
|
+
export1_raw = await self._export_all(deployed, environment_names)
|
|
114
|
+
export1 = {name: normalize(data) for name, data in export1_raw.items()}
|
|
115
|
+
|
|
116
|
+
# Phase 4: Create artifacts from export #1
|
|
117
|
+
logger.info("Creating artifacts from export #1...")
|
|
118
|
+
exported_universe = await asyncio.to_thread(
|
|
119
|
+
self._create_universe_artifact, environment_artifacts, export1_raw, deployed.env_version, universe.version
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Phase 5: Reload from exported artifacts
|
|
123
|
+
logger.info("Reloading from exported artifacts...")
|
|
124
|
+
await env.load_environment_universe_artifact(exported_universe)
|
|
125
|
+
|
|
126
|
+
# Phase 6: Export #2
|
|
127
|
+
logger.info("Exporting state (round 2)...")
|
|
128
|
+
export2_raw = await self._export_all(deployed, environment_names)
|
|
129
|
+
export2 = {name: normalize(data) for name, data in export2_raw.items()}
|
|
130
|
+
|
|
131
|
+
# Phase 7: Compare
|
|
132
|
+
logger.info("Comparing results...")
|
|
133
|
+
# "environments" is the SAME dict object as "services" (dual-write), so every later
|
|
134
|
+
# mutation — the loop below, and apply_judge_verdict's in-place merge — lands in both.
|
|
135
|
+
per_environment: dict[str, Any] = {}
|
|
136
|
+
result: dict[str, Any] = {"compatible": True, "services": per_environment, "environments": per_environment, "exported_universe_artifact_id": exported_universe.id}
|
|
137
|
+
for name in environment_names:
|
|
138
|
+
load_issues = compare_dicts(originals[name], export1[name], "universe", "export1") if name in originals else []
|
|
139
|
+
idempotency_issues = compare_dicts(export1[name], export2[name], "export1", "export2")
|
|
140
|
+
annotated, is_compatible = classify_issues(load_issues, idempotency_issues)
|
|
141
|
+
if not is_compatible:
|
|
142
|
+
result["compatible"] = False
|
|
143
|
+
result["services"][name] = {"compatible": is_compatible, "issues": annotated}
|
|
144
|
+
critical_count = sum(1 for i in annotated if i["critical"])
|
|
145
|
+
logger.info(f" {name}: {'COMPATIBLE' if is_compatible else 'INCOMPATIBLE'} ({len(annotated)} issues, {critical_count} critical)")
|
|
146
|
+
|
|
147
|
+
# Phase 7b: Optionally bundle original/export1/export2 JSON into a FileArtifactUniverse so a
|
|
148
|
+
# downstream agent-judge can diff them on its filesystem. Additive — does not touch
|
|
149
|
+
# the programmatic verdict above.
|
|
150
|
+
if self.emit_file_artifact_universe:
|
|
151
|
+
fau = await asyncio.to_thread(
|
|
152
|
+
self._create_file_artifact_universe,
|
|
153
|
+
environment_artifacts, exported_universe, export2_raw, deployed.env_version, universe.version,
|
|
154
|
+
)
|
|
155
|
+
result["file_artifact_universe_id"] = fau.id
|
|
156
|
+
logger.info(f"Emitted FileArtifactUniverse for agent-judge: {fau.id}")
|
|
157
|
+
|
|
158
|
+
# Phase 8: Always expose the programmatic result in context (downstream steps read it);
|
|
159
|
+
# only persist to the env-artifact store when this step owns the write (persist_result).
|
|
160
|
+
# When a CombineUniverseVerdictsStep follows, it is the single authoritative writer.
|
|
161
|
+
context.metadata.setdefault("verifications", {})[EnvArtifactType.UNIVERSE_COMPATIBILITY] = result
|
|
162
|
+
if self.persist_result:
|
|
163
|
+
get_env_artifact_store().put(env_id=self.env_id, env_version=deployed.env_version, artifact_id=universe.id, artifact_version=universe.version, type=EnvArtifactType.UNIVERSE_COMPATIBILITY, data=result)
|
|
164
|
+
|
|
165
|
+
return context
|
|
166
|
+
|
|
167
|
+
def _create_file_artifact_universe(self, original_environment_artifacts: list, exported_universe: Any, export2_raw: dict[str, dict], env_version: int, universe_version: int) -> Any:
|
|
168
|
+
"""Bundle the three universe snapshots into one FileArtifactUniverse for the agent-judge.
|
|
169
|
+
|
|
170
|
+
Faithfulness (this validator measures lossiness, so the bundle must not transform data):
|
|
171
|
+
- ``original/<svc>.json`` references the universe's EXISTING per-service FileArtifacts
|
|
172
|
+
(verbatim bytes — what compare_dicts loaded), no re-serialization.
|
|
173
|
+
- ``export_1/<svc>.json`` references the FileArtifacts the export1 universe was built from
|
|
174
|
+
(the exact bytes the programmatic verdict used).
|
|
175
|
+
- ``export_2/<svc>.json`` is materialized fresh (export2 is not otherwise persisted), dumped
|
|
176
|
+
from the same in-memory raw dict, with the same serialization as export1
|
|
177
|
+
(``default=str`` for parity; ``ensure_ascii=False`` so the judge reads real unicode).
|
|
178
|
+
"""
|
|
179
|
+
from agent_env.artifact import FileArtifact, FileArtifactUniverse
|
|
180
|
+
|
|
181
|
+
prefix = f"validate-{self.env_id}-v{env_version}-{self.universe_artifact_id}-v{universe_version}"
|
|
182
|
+
with artifact_write_lock(prefix):
|
|
183
|
+
file_artifacts: dict[str, Any] = {}
|
|
184
|
+
for sa in original_environment_artifacts:
|
|
185
|
+
file_artifacts[f"original/{sa.environment_name}.json"] = sa.get_file_artifact()
|
|
186
|
+
for sa in exported_universe.get_environment_artifacts():
|
|
187
|
+
file_artifacts[f"export_1/{sa.environment_name}.json"] = sa.get_file_artifact()
|
|
188
|
+
for name, data in export2_raw.items():
|
|
189
|
+
# Cleanup wraps both the dump and the put, so a failure in either can't leak the temp
|
|
190
|
+
# file; mkstemp yields the path up front so `finally` always has a valid path to unlink.
|
|
191
|
+
fd, tmp_path = tempfile.mkstemp(suffix=".json", prefix=f"export2-{name}-")
|
|
192
|
+
try:
|
|
193
|
+
with os.fdopen(fd, "w") as f:
|
|
194
|
+
json.dump(data, f, default=str, ensure_ascii=False)
|
|
195
|
+
fa = FileArtifact.put(id=f"{prefix}-export2-{name}", description=f"Export #2 of {name} for universe-judge", file_path=tmp_path)
|
|
196
|
+
finally:
|
|
197
|
+
os.unlink(tmp_path)
|
|
198
|
+
file_artifacts[f"export_2/{name}.json"] = fa
|
|
199
|
+
|
|
200
|
+
return FileArtifactUniverse.put(
|
|
201
|
+
id=self.file_artifact_universe_id(self.env_id, env_version, self.universe_artifact_id, universe_version),
|
|
202
|
+
file_artifacts=file_artifacts,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
async def _export_all(deployed: DeployedEnv, environment_names: list[str]) -> dict[str, dict]:
|
|
207
|
+
"""Export each service: v1 ``data/get`` at the base the env card gives it, else legacy ``GET /export-state``."""
|
|
208
|
+
from agent_env.env import legacy_protocol
|
|
209
|
+
from agent_env.env.env import gateway_url_of
|
|
210
|
+
from agentenv_protocol import client as protocol_v1
|
|
211
|
+
result = {}
|
|
212
|
+
for name in environment_names:
|
|
213
|
+
base_url = await legacy_protocol.v1_base_url(deployed, gateway_url_of(deployed), name)
|
|
214
|
+
if base_url is not None:
|
|
215
|
+
resp = await protocol_v1.get_data(base_url)
|
|
216
|
+
result[name] = resp.parts[0].data if resp.parts else {}
|
|
217
|
+
else:
|
|
218
|
+
result[name] = await legacy_protocol.export_state(gateway_url_of(deployed), name)
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
def _create_universe_artifact(self, original_environment_artifacts: list, export_data: dict[str, dict], env_version: int, universe_version: int) -> Any:
|
|
222
|
+
"""Create FileArtifact + EnvironmentArtifact per service, bundle into EnvironmentUniverseArtifact."""
|
|
223
|
+
from agent_env.artifact import FileArtifact, EnvironmentArtifact, EnvironmentUniverseArtifact
|
|
224
|
+
prefix = f"validate-{self.env_id}-v{env_version}-{self.universe_artifact_id}-v{universe_version}"
|
|
225
|
+
with artifact_write_lock(prefix):
|
|
226
|
+
export_environment_artifacts = []
|
|
227
|
+
for sa in original_environment_artifacts:
|
|
228
|
+
name = sa.environment_name
|
|
229
|
+
if name not in export_data:
|
|
230
|
+
continue
|
|
231
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, prefix=f"export1-{name}-") as f:
|
|
232
|
+
json.dump(export_data[name], f, default=str)
|
|
233
|
+
tmp_path = f.name
|
|
234
|
+
try:
|
|
235
|
+
file_artifact = FileArtifact.put(id=f"{prefix}-export1-{name}", description=f"Export #1 of {name} for universe compat validation", file_path=tmp_path)
|
|
236
|
+
finally:
|
|
237
|
+
os.unlink(tmp_path)
|
|
238
|
+
svc_artifact = EnvironmentArtifact.put(id=f"{prefix}-export1-svc-{name}", environment_name=sa.environment_name, file_artifact=file_artifact)
|
|
239
|
+
export_environment_artifacts.append(svc_artifact)
|
|
240
|
+
|
|
241
|
+
return EnvironmentUniverseArtifact.put(id=f"{prefix}-export1", environment_artifacts=export_environment_artifacts)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Push routing tables to deployed A2A agents' /ext/peer-agents extension."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import dataclasses
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import ClassVar, Optional
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from agent_env.task_step.context import DeployedAgent, TaskStepContext
|
|
13
|
+
from agent_env.task_step.task_step import TaskStep, TaskStepDependency
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
_POST_TIMEOUT_SECONDS = 30
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AgentPeers:
|
|
21
|
+
source_agent_name: str
|
|
22
|
+
peer_agent_names: list[str]
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_dict(cls, data: dict) -> "AgentPeers":
|
|
26
|
+
return cls(source_agent_name=data["source_agent_name"], peer_agent_names=list(data["peer_agent_names"]))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PeerAgentsTaskStep(TaskStep):
|
|
30
|
+
type: ClassVar[str] = "peer_agents"
|
|
31
|
+
entity_refs = ()
|
|
32
|
+
|
|
33
|
+
def __init__(self, id: str, version: Optional[int], peerings: list, depends_on: Optional[list[TaskStepDependency]] = None, fail_task_on_error: bool = True):
|
|
34
|
+
super().__init__(id, version, depends_on=depends_on, fail_task_on_error=fail_task_on_error)
|
|
35
|
+
self.peerings = [p if isinstance(p, AgentPeers) else AgentPeers.from_dict(p) for p in peerings]
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
return {**super().to_dict(), "peerings": [dataclasses.asdict(p) for p in self.peerings]}
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(cls, data: dict) -> "PeerAgentsTaskStep":
|
|
42
|
+
return cls(**cls._base_from_dict(data), peerings=[AgentPeers.from_dict(p) for p in data["peerings"]])
|
|
43
|
+
|
|
44
|
+
async def execute(self, context: TaskStepContext) -> TaskStepContext:
|
|
45
|
+
from agent_env.a2a_agent import A2AAgent
|
|
46
|
+
if not self.peerings:
|
|
47
|
+
raise RuntimeError("PeerAgentsTaskStep: no peerings supplied")
|
|
48
|
+
deployed_by_name = {a.agent_name: a for a in context.deployed_agents}
|
|
49
|
+
self._validate(deployed_by_name, A2AAgent)
|
|
50
|
+
await asyncio.gather(*[self._post(deployed_by_name, p, A2AAgent) for p in self.peerings])
|
|
51
|
+
recorded = context.metadata.setdefault("agent_peerings", [])
|
|
52
|
+
for p in self.peerings:
|
|
53
|
+
recorded.append({"source_agent_name": p.source_agent_name, "peer_agent_names": list(p.peer_agent_names)})
|
|
54
|
+
logger.info("PeerAgentsTaskStep: peered %d source(s)", len(self.peerings))
|
|
55
|
+
return context
|
|
56
|
+
|
|
57
|
+
def _validate(self, deployed_by_name: dict[str, DeployedAgent], a2a_agent_cls) -> None:
|
|
58
|
+
seen: set[str] = set()
|
|
59
|
+
for p in self.peerings:
|
|
60
|
+
src = p.source_agent_name
|
|
61
|
+
if src in seen:
|
|
62
|
+
raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' listed twice")
|
|
63
|
+
seen.add(src)
|
|
64
|
+
agent = deployed_by_name.get(src)
|
|
65
|
+
if agent is None:
|
|
66
|
+
raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' not in deployed_agents (available: {sorted(deployed_by_name)})")
|
|
67
|
+
if not (agent.a2a_url or agent.api_url):
|
|
68
|
+
raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' has no a2a_url")
|
|
69
|
+
if a2a_agent_cls.find_extension(agent.a2a_card or {}, a2a_agent_cls.EXT_PEER_AGENTS) is None:
|
|
70
|
+
raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' does not advertise the peer-agents extension")
|
|
71
|
+
if src in p.peer_agent_names:
|
|
72
|
+
raise RuntimeError(f"PeerAgentsTaskStep: source '{src}' lists itself as a peer")
|
|
73
|
+
missing = [n for n in p.peer_agent_names if n not in deployed_by_name]
|
|
74
|
+
if missing:
|
|
75
|
+
raise RuntimeError(f"PeerAgentsTaskStep: peers {missing} for source '{src}' not in deployed_agents")
|
|
76
|
+
|
|
77
|
+
async def _post(self, deployed_by_name: dict[str, DeployedAgent], peering: AgentPeers, a2a_agent_cls) -> None:
|
|
78
|
+
src = deployed_by_name[peering.source_agent_name]
|
|
79
|
+
a2a_url = src.a2a_url or src.api_url
|
|
80
|
+
ext = a2a_agent_cls.find_extension(src.a2a_card or {}, a2a_agent_cls.EXT_PEER_AGENTS)
|
|
81
|
+
endpoint = (ext.get("params") or {}).get("endpoint", "/ext/peer-agents")
|
|
82
|
+
peers = [self._record(deployed_by_name[n]) for n in peering.peer_agent_names]
|
|
83
|
+
async with httpx.AsyncClient() as client:
|
|
84
|
+
resp = await client.post(a2a_url + endpoint, json={"peers": peers}, timeout=_POST_TIMEOUT_SECONDS)
|
|
85
|
+
if resp.status_code >= 400:
|
|
86
|
+
raise RuntimeError(f"PeerAgentsTaskStep: POST {a2a_url + endpoint} failed for '{peering.source_agent_name}': {resp.status_code} {resp.text}")
|
|
87
|
+
logger.info("PeerAgentsTaskStep: peered '%s' with %s", peering.source_agent_name, peering.peer_agent_names)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _record(peer: DeployedAgent) -> dict:
|
|
91
|
+
card = peer.a2a_card or {}
|
|
92
|
+
return {"name": peer.agent_name, "url": peer.a2a_url or peer.api_url, "card": card, "description": card.get("description")}
|