agentenv-framework 0.9.1254__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
agent_env/env/env.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import posixpath
|
|
5
|
+
import shlex
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Self
|
|
9
|
+
|
|
10
|
+
from agentenv_protocol import client as protocol_v1
|
|
11
|
+
|
|
12
|
+
from agent_env.entity_refs import EntityRef
|
|
13
|
+
from agent_env.utils.paths import validate_relative_filename
|
|
14
|
+
|
|
15
|
+
from .gateway import GatewayMode
|
|
16
|
+
from .gateway.constants import WELL_KNOWN_PATH
|
|
17
|
+
from .store import get_env_store
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Default timeout for one invoke() call; above the class because a default argument needs it.
|
|
22
|
+
_EXTENSION_CALL_TIMEOUT_S = 30
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from agent_env.bundle.authoring import AuthoringContext
|
|
26
|
+
from .store import EnvQuery
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EnvCapabilityUnsupported(RuntimeError):
|
|
30
|
+
"""A deployed env's card does not offer a capability method its caller needs."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, capability: str, method: str, env_id: str, instance_id: Optional[str] = None,
|
|
33
|
+
environment_name: Optional[str] = None) -> None:
|
|
34
|
+
self.capability = capability
|
|
35
|
+
self.method = method
|
|
36
|
+
self.env_id = env_id
|
|
37
|
+
self.instance_id = instance_id
|
|
38
|
+
self.environment_name = environment_name
|
|
39
|
+
child = f" child env '{environment_name}'" if environment_name else ""
|
|
40
|
+
super().__init__(f"env '{env_id}'{child} does not offer '{method}' on {capability}.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EnvNeedsGateway(RuntimeError):
|
|
44
|
+
"""A caller reaches an env through its gateway, and the env was deployed without one."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, what: str, env_id: Optional[str] = None) -> None:
|
|
47
|
+
self.what = what
|
|
48
|
+
self.env_id = env_id
|
|
49
|
+
env = f"env '{env_id}'" if env_id else "this env"
|
|
50
|
+
super().__init__(f"{what} needs a gateway; {env} was deployed without one")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EnvNeedsSandbox(RuntimeError):
|
|
54
|
+
"""A caller acts on an env's sandbox, and the env runs outside agent-env's sandboxes."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, what: str, env_id: str) -> None:
|
|
57
|
+
self.what = what
|
|
58
|
+
self.env_id = env_id
|
|
59
|
+
super().__init__(f"{what} needs a sandbox; env '{env_id}' runs outside agent-env's sandboxes")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(kw_only=True)
|
|
63
|
+
class DeployedEnv:
|
|
64
|
+
"""A deployed env: the env, its card, and the registry's bookkeeping. Each topology's subclass adds what it created."""
|
|
65
|
+
|
|
66
|
+
env_id: str
|
|
67
|
+
env_version: int
|
|
68
|
+
# The provider that made the record, so the class it loads as (see from_dict); capabilities still come from the card.
|
|
69
|
+
env_provider_type: Optional[str] = None
|
|
70
|
+
environment_card_url: Optional[str] = None
|
|
71
|
+
# The env card as last read, and when (ISO 8601, UTC); written at deploy from the readiness probe.
|
|
72
|
+
environment_card: Optional[dict] = None
|
|
73
|
+
environment_card_read_at_utc: Optional[str] = None
|
|
74
|
+
# From the card whenever the record carries it (__post_init__); stored because readers of the JSON use them.
|
|
75
|
+
mcp_url: Optional[str] = None
|
|
76
|
+
mcp_server_name: Optional[str] = None
|
|
77
|
+
# Set by the registry and the deploy step, never by a provider.
|
|
78
|
+
metadata: Optional[dict] = None
|
|
79
|
+
instance_id: Optional[str] = None
|
|
80
|
+
created_at_utc: Optional[str] = None
|
|
81
|
+
expires_at_utc: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
# Only records from before #1137 (2026-09-24) lack their card: they keep the stored values.
|
|
85
|
+
if self.environment_card and self.environment_card_url:
|
|
86
|
+
self.mcp_url = _mcp_url(self.environment_url, self.environment_card)
|
|
87
|
+
self.mcp_server_name = self.environment_card.get("name")
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_dict(cls, data: dict) -> DeployedEnv:
|
|
91
|
+
record_class = _record_class(data)
|
|
92
|
+
return record_class(**record_class._fields_from(data))
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def environment_url(self) -> Optional[str]:
|
|
96
|
+
"""The env's address: its card URL without the well-known path; card paths join onto it."""
|
|
97
|
+
return _environment_url(self.environment_card_url) if self.environment_card_url else None
|
|
98
|
+
|
|
99
|
+
def supports(self, uri: str, method: str) -> bool:
|
|
100
|
+
"""Whether the stored env card offers `method` on the extension at `uri`; no network."""
|
|
101
|
+
return protocol_v1.find_extension_method(self.environment_card or {}, uri, method) is not None
|
|
102
|
+
|
|
103
|
+
def require(self, uri: str, method: str) -> None:
|
|
104
|
+
if not self.supports(uri, method):
|
|
105
|
+
raise EnvCapabilityUnsupported(uri, method, self.env_id, self.instance_id)
|
|
106
|
+
|
|
107
|
+
def get_child_env_card(self, environment_name: str) -> Optional[dict]:
|
|
108
|
+
"""The stored card's child env named `environment_name`; a leaf card is its own only child env."""
|
|
109
|
+
card = self.environment_card or {}
|
|
110
|
+
if not isinstance(card.get("children_environments"), list):
|
|
111
|
+
return card if card.get("name") == environment_name else None
|
|
112
|
+
return protocol_v1.find_child(card, environment_name)
|
|
113
|
+
|
|
114
|
+
async def invoke(self, uri: str, method: str, params: Optional[dict] = None, *,
|
|
115
|
+
environment_name: Optional[str] = None, timeout: int = _EXTENSION_CALL_TIMEOUT_S) -> Any:
|
|
116
|
+
"""Call `method` on the extension at `uri` as the env card, or a child env's card, advertises it."""
|
|
117
|
+
card = self.environment_card if environment_name is None else self.get_child_env_card(environment_name)
|
|
118
|
+
if card is None or not self.environment_url or protocol_v1.find_extension_method(card, uri, method) is None:
|
|
119
|
+
raise EnvCapabilityUnsupported(uri, method, self.env_id, self.instance_id, environment_name)
|
|
120
|
+
return await protocol_v1.invoke_extension(self.environment_url, card, uri, params, timeout, method=method)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def _fields_from(cls, data: dict) -> dict:
|
|
124
|
+
return dict(
|
|
125
|
+
env_id=data["env_id"],
|
|
126
|
+
env_version=data["env_version"],
|
|
127
|
+
env_provider_type=data.get("env_provider_type"),
|
|
128
|
+
environment_card_url=data.get("environment_card_url"),
|
|
129
|
+
environment_card=data.get("environment_card"),
|
|
130
|
+
environment_card_read_at_utc=data.get("environment_card_read_at_utc"),
|
|
131
|
+
mcp_url=data.get("mcp_url"),
|
|
132
|
+
mcp_server_name=data.get("mcp_server_name"),
|
|
133
|
+
metadata=data.get("metadata"),
|
|
134
|
+
instance_id=data.get("instance_id"),
|
|
135
|
+
created_at_utc=data.get("created_at_utc"),
|
|
136
|
+
expires_at_utc=data.get("expires_at_utc"),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(kw_only=True)
|
|
141
|
+
class DeployedSandboxEnv(DeployedEnv):
|
|
142
|
+
"""A deployed env running in our sandboxes: what restore, teardown and the reapers act on."""
|
|
143
|
+
|
|
144
|
+
sandbox_id: str
|
|
145
|
+
sandbox_type: Optional[str] = None
|
|
146
|
+
sandbox_ids: dict[str, str | dict[str, str]] = field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def _fields_from(cls, data: dict) -> dict:
|
|
150
|
+
return {
|
|
151
|
+
**super()._fields_from(data),
|
|
152
|
+
"sandbox_id": data.get("sandbox_id"),
|
|
153
|
+
"sandbox_type": data.get("sandbox_type"),
|
|
154
|
+
"sandbox_ids": data.get("sandbox_ids") or {},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(kw_only=True)
|
|
159
|
+
class DeployedGatewayEnv(DeployedSandboxEnv):
|
|
160
|
+
"""A deployed env fronted by a gateway: the gateway's URLs and mode, and the state store its deploy holds."""
|
|
161
|
+
|
|
162
|
+
env_provider_type: Optional[str] = "gateway"
|
|
163
|
+
gateway_url: str
|
|
164
|
+
gateway_mode: str = GatewayMode.PERFORMANCE.value
|
|
165
|
+
db_web_url: Optional[str] = None
|
|
166
|
+
db_mcp_url: Optional[str] = None
|
|
167
|
+
website_frontend_urls: Optional[dict[str, str]] = None
|
|
168
|
+
vnc_url: Optional[str] = None
|
|
169
|
+
# Ids of the EnvStateInstance record(s) this deploy's state was provisioned into
|
|
170
|
+
# (one today — one backend per deploy). The forward pointer deploy -> state store.
|
|
171
|
+
env_state_instance_ids: list[str] = field(default_factory=list)
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def _fields_from(cls, data: dict) -> dict:
|
|
175
|
+
return {
|
|
176
|
+
**super()._fields_from(data),
|
|
177
|
+
"env_provider_type": data.get("env_provider_type") or "gateway",
|
|
178
|
+
"gateway_url": data.get("gateway_url"),
|
|
179
|
+
"gateway_mode": data.get("gateway_mode", GatewayMode.PERFORMANCE.value),
|
|
180
|
+
"db_web_url": data.get("db_web_url"),
|
|
181
|
+
"db_mcp_url": data.get("db_mcp_url"),
|
|
182
|
+
"website_frontend_urls": data.get("website_frontend_urls"),
|
|
183
|
+
"vnc_url": data.get("vnc_url"),
|
|
184
|
+
"env_state_instance_ids": data.get("env_state_instance_ids") or [],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class LoadEnvironmentUniverseArtifactResult:
|
|
190
|
+
metadata_filepaths: dict[str, str] = field(default_factory=dict)
|
|
191
|
+
# Which path the load actually took. A snapshot restore swaps a pre-ingested servicedb
|
|
192
|
+
# image; a miss re-ingests every service over HTTP under a 600s per-service cap. The
|
|
193
|
+
# two are comparable in wall-clock on a well-sized VM (measured ~4min vs ~3.5min on a
|
|
194
|
+
# 13-service / ~8.7GB universe) -- what differs is the risk: only the re-ingest can
|
|
195
|
+
# blow a timeout, and only it needs the CPU. Reported so callers can tell the two
|
|
196
|
+
# apart, since otherwise an identical step silently costs minutes or fails.
|
|
197
|
+
restored_from_snapshot: bool = False
|
|
198
|
+
snapshot_db_image_artifact_id: str | None = None
|
|
199
|
+
# Set only on the re-ingest path: whether a reusable snapshot was baked afterwards, so
|
|
200
|
+
# the *next* load can restore instead. None means no bake was attempted.
|
|
201
|
+
snapshot_baked: bool | None = None
|
|
202
|
+
snapshot_bake_error: str | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class LoadFileArtifactUniverseResult:
|
|
207
|
+
"""Records what was staged onto the env sandbox by load_file_artifact_universe."""
|
|
208
|
+
|
|
209
|
+
destination_path: str
|
|
210
|
+
files: dict[str, str] = field(default_factory=dict) # filename -> absolute dest path
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Env(ABC):
|
|
214
|
+
"""Base class for all envs."""
|
|
215
|
+
|
|
216
|
+
description: ClassVar[str]
|
|
217
|
+
type: ClassVar[str] = "env"
|
|
218
|
+
toml_refs: ClassVar[tuple[EntityRef, ...]] = ()
|
|
219
|
+
env_provider_types: ClassVar[tuple[str, ...]] = ()
|
|
220
|
+
|
|
221
|
+
def __init__(self, id: str, version: Optional[int], metadata: Optional[dict[str, Any]] = None):
|
|
222
|
+
self.id: str = id
|
|
223
|
+
self.version: Optional[int] = version
|
|
224
|
+
self.metadata: dict[str, Any] = metadata or {}
|
|
225
|
+
self._instance_id: Optional[str] = None
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def get(cls, id: str, version: Optional[int] = None) -> "Env":
|
|
229
|
+
from .store import get_env_store
|
|
230
|
+
return get_env_store().get(id, version)
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
async def from_instance_id(cls, instance_id: str) -> "Env":
|
|
234
|
+
from .store import get_env_instance_store
|
|
235
|
+
deployed = get_env_instance_store().get(instance_id)
|
|
236
|
+
env = cls.get(deployed.env_id, deployed.env_version)
|
|
237
|
+
return await type(env).from_deployed_env(deployed)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def put(cls, **kwargs: Any) -> Self:
|
|
241
|
+
from .store import get_env_store
|
|
242
|
+
kwargs.setdefault("version", None)
|
|
243
|
+
instance = cls(**kwargs)
|
|
244
|
+
return get_env_store().put_document(instance)
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def query(cls) -> "EnvQuery":
|
|
248
|
+
from .store import EnvQuery, get_env_store
|
|
249
|
+
return EnvQuery(get_env_store())
|
|
250
|
+
|
|
251
|
+
def update_metadata(self, new_metadata: dict[str, Any]) -> None:
|
|
252
|
+
"""Replace this env's metadata in the database using compare-and-swap.
|
|
253
|
+
|
|
254
|
+
The current in-memory metadata is used as the expected old value.
|
|
255
|
+
On success, self.metadata is updated to new_metadata.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
ValueError: If the env has not been saved (version is None).
|
|
259
|
+
ConcurrentModificationError: If metadata was modified since this env was loaded.
|
|
260
|
+
"""
|
|
261
|
+
from .store import get_env_store
|
|
262
|
+
if self.version is None:
|
|
263
|
+
raise ValueError("Cannot update metadata on an unsaved env (version is None)")
|
|
264
|
+
self.metadata = get_env_store().update_metadata(self.id, self.version, self.metadata, new_metadata)
|
|
265
|
+
|
|
266
|
+
def merge_metadata(self, updates: dict[str, Any], retries: int = 3) -> None:
|
|
267
|
+
"""Merge ``updates`` into this env's metadata, re-reading and retrying on a
|
|
268
|
+
lost CAS. Use instead of ``update_metadata`` when writing one key of a doc
|
|
269
|
+
another writer may be updating concurrently.
|
|
270
|
+
"""
|
|
271
|
+
from agent_env.store.base import ConcurrentModificationError
|
|
272
|
+
from .store import get_env_store
|
|
273
|
+
if self.version is None:
|
|
274
|
+
raise ValueError("Cannot update metadata on an unsaved env (version is None)")
|
|
275
|
+
metadata = self.metadata
|
|
276
|
+
for attempt in range(retries + 1):
|
|
277
|
+
try:
|
|
278
|
+
self.metadata = get_env_store().update_metadata(
|
|
279
|
+
self.id, self.version, metadata, {**metadata, **updates}
|
|
280
|
+
)
|
|
281
|
+
return
|
|
282
|
+
except ConcurrentModificationError:
|
|
283
|
+
if attempt == retries:
|
|
284
|
+
raise
|
|
285
|
+
metadata = Env.get(self.id, self.version).metadata
|
|
286
|
+
|
|
287
|
+
def to_dict(self) -> dict[str, Any]:
|
|
288
|
+
return {"id": self.id, "type": self.type, "version": self.version, "metadata": self.metadata}
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def from_dict(cls, data: dict[str, Any]) -> "Env":
|
|
292
|
+
raise NotImplementedError(f"{cls.__name__} must implement from_dict")
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def from_toml(cls, data: dict[str, Any], ctx: AuthoringContext) -> "Env":
|
|
296
|
+
"""Write the env authored as ``data`` (its toml, with the keys ``toml_refs`` declares resolved
|
|
297
|
+
to ids) under ``ctx.id`` and return it. The default reads ``data`` as ``from_dict`` reads a
|
|
298
|
+
stored document; a type whose toml differs overrides this."""
|
|
299
|
+
return get_env_store().put_document(cls.from_dict({**data, "id": ctx.id, "version": None}))
|
|
300
|
+
|
|
301
|
+
async def deploy(self, **kwargs) -> DeployedEnv:
|
|
302
|
+
raise NotImplementedError(f"{type(self).__name__} must implement deploy()")
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
async def from_deployed_env(cls, deployed: DeployedEnv) -> Self:
|
|
306
|
+
"""The env a running deployment serves, for its caller to drive. A type that can be
|
|
307
|
+
deployed on its own overrides this; one deployed only inside another, or never, need not.
|
|
308
|
+
An override takes ``deployed: DeployedEnv`` and narrows it with ``isinstance``.
|
|
309
|
+
"""
|
|
310
|
+
raise NotImplementedError(f"{cls.__name__} cannot be rebuilt from a deployment")
|
|
311
|
+
|
|
312
|
+
async def reset(self, deployed: "DeployedEnv") -> None:
|
|
313
|
+
"""Return an already-deployed env to a clean between-tasks state.
|
|
314
|
+
|
|
315
|
+
Base raises so the ``reset_env`` step treats it as unsupported; envs that
|
|
316
|
+
can reset in place override this.
|
|
317
|
+
"""
|
|
318
|
+
raise NotImplementedError(f"{type(self).__name__} does not support reset()")
|
|
319
|
+
|
|
320
|
+
async def load_file_artifact_universe(
|
|
321
|
+
self,
|
|
322
|
+
file_artifact_universe: "Any",
|
|
323
|
+
destination_path: Optional[str] = None,
|
|
324
|
+
) -> "LoadFileArtifactUniverseResult":
|
|
325
|
+
"""Stage every FileArtifact in `file_artifact_universe` onto this env's sandbox VM.
|
|
326
|
+
|
|
327
|
+
Files land at ``<destination>/<filename>``. Nested relative paths
|
|
328
|
+
(e.g. ``subdir/file.txt``) are supported — parent dirs are created
|
|
329
|
+
as needed. Each file is fetched directly from S3 by the sandbox via
|
|
330
|
+
a presigned URL, so we never round-trip the bytes through this
|
|
331
|
+
process.
|
|
332
|
+
|
|
333
|
+
Default implementation uses ``self._sandbox`` (set by ``deploy()`` /
|
|
334
|
+
``from_deployed_env()`` on most env subclasses). Subclasses that
|
|
335
|
+
manage several sandboxes, or none, should override.
|
|
336
|
+
"""
|
|
337
|
+
sandbox = getattr(self, "_sandbox", None)
|
|
338
|
+
if sandbox is None:
|
|
339
|
+
raise RuntimeError(
|
|
340
|
+
f"{type(self).__name__} has no _sandbox attribute; either call "
|
|
341
|
+
"deploy() / from_deployed_env() first or override "
|
|
342
|
+
"load_file_artifact_universe in this env class"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
destination = (destination_path or "/tmp/file_artifacts").rstrip("/") or "/"
|
|
346
|
+
|
|
347
|
+
file_artifacts = file_artifact_universe.get_file_artifacts()
|
|
348
|
+
if not file_artifacts:
|
|
349
|
+
logger.warning(
|
|
350
|
+
f"FileArtifactUniverse '{file_artifact_universe.id}' "
|
|
351
|
+
f"v{file_artifact_universe.version} has no files; nothing to load"
|
|
352
|
+
)
|
|
353
|
+
return LoadFileArtifactUniverseResult(destination_path=destination, files={})
|
|
354
|
+
|
|
355
|
+
dirs_to_make = {destination}
|
|
356
|
+
loaded: dict[str, str] = {}
|
|
357
|
+
for filename in file_artifacts:
|
|
358
|
+
validate_relative_filename(filename)
|
|
359
|
+
dest_path = posixpath.join(destination, filename)
|
|
360
|
+
parent = posixpath.dirname(dest_path)
|
|
361
|
+
if parent:
|
|
362
|
+
dirs_to_make.add(parent)
|
|
363
|
+
loaded[filename] = dest_path
|
|
364
|
+
|
|
365
|
+
mkdir_cmd = " && ".join(f"mkdir -p {shlex.quote(d)}" for d in sorted(dirs_to_make))
|
|
366
|
+
await sandbox.exec_script(mkdir_cmd)
|
|
367
|
+
|
|
368
|
+
total = len(file_artifacts)
|
|
369
|
+
logger.info(
|
|
370
|
+
f"Loading FileArtifactUniverse '{file_artifact_universe.id}' "
|
|
371
|
+
f"v{file_artifact_universe.version} ({total} file(s)) at {destination}"
|
|
372
|
+
)
|
|
373
|
+
for idx, (filename, file_artifact) in enumerate(file_artifacts.items(), 1):
|
|
374
|
+
dest_path = loaded[filename]
|
|
375
|
+
logger.info(f" [{idx}/{total}] {file_artifact.object_url} -> {dest_path}")
|
|
376
|
+
await sandbox.load_s3_file(file_artifact.object_url, dest_path)
|
|
377
|
+
|
|
378
|
+
logger.info(
|
|
379
|
+
f"Loaded FileArtifactUniverse '{file_artifact_universe.id}' "
|
|
380
|
+
f"v{file_artifact_universe.version}: {total} file(s) at {destination}"
|
|
381
|
+
)
|
|
382
|
+
return LoadFileArtifactUniverseResult(destination_path=destination, files=loaded)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def gateway_url_of(deployed: Optional[DeployedEnv]) -> Optional[str]:
|
|
386
|
+
"""The gateway's URL, or None for a record without a gateway."""
|
|
387
|
+
return deployed.gateway_url if isinstance(deployed, DeployedGatewayEnv) else None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def require_gateway_url(deployed: DeployedEnv, what: str) -> str:
|
|
391
|
+
"""The gateway's URL, or EnvNeedsGateway naming `what` for a record without a gateway."""
|
|
392
|
+
if url := gateway_url_of(deployed):
|
|
393
|
+
return url
|
|
394
|
+
raise EnvNeedsGateway(what, deployed.env_id)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def require_sandbox(deployed: DeployedEnv, what: str) -> DeployedSandboxEnv:
|
|
398
|
+
"""The record of an env in one of agent-env's sandboxes, or EnvNeedsSandbox naming `what` for one outside them."""
|
|
399
|
+
if isinstance(deployed, DeployedSandboxEnv):
|
|
400
|
+
return deployed
|
|
401
|
+
raise EnvNeedsSandbox(what, deployed.env_id)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _environment_url(card_url: str) -> str:
|
|
405
|
+
"""The env's address: its card URL without the well-known path, where the protocol puts the card."""
|
|
406
|
+
return card_url.removesuffix(WELL_KNOWN_PATH).rstrip("/")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _mcp_url(url: str, card: dict) -> str:
|
|
410
|
+
"""The MCP endpoint the card declares, joined onto the env's URL with exactly one slash.
|
|
411
|
+
|
|
412
|
+
Plain joining, not URL resolution: resolving "/mcp" would drop a path prefix such as a sandbox
|
|
413
|
+
proxy's /sandbox/<id>, and resolving a relative path would replace the URL's last segment.
|
|
414
|
+
"""
|
|
415
|
+
return f"{url.rstrip('/')}/{protocol_v1.mcp_path(card).lstrip('/')}"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _record_class(data: dict) -> type[DeployedEnv]:
|
|
419
|
+
"""A stored record's class: its provider type's, else its shape's, so a record from before the type (or of one unknown here) keeps every field."""
|
|
420
|
+
from agent_env.providers.env_providers.env_provider import record_class_for # providers import this module
|
|
421
|
+
|
|
422
|
+
provider_type = data.get("env_provider_type")
|
|
423
|
+
record_class = record_class_for(provider_type)
|
|
424
|
+
if record_class is not None:
|
|
425
|
+
return record_class
|
|
426
|
+
if any(data.get(name) for name in _GATEWAY_ONLY_FIELDS):
|
|
427
|
+
return DeployedGatewayEnv
|
|
428
|
+
# Every record from before the type ran in a sandbox; only a newer kernel's type can mean none.
|
|
429
|
+
return DeployedSandboxEnv if data.get("sandbox_id") or not provider_type else DeployedEnv
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# A record carrying any of these is a gateway's. Not gateway_mode: an older kernel writes its default into any record it round-trips.
|
|
433
|
+
_GATEWAY_ONLY_FIELDS = ("gateway_url", "db_web_url", "db_mcp_url", "website_frontend_urls", "vnc_url", "env_state_instance_ids")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Store for env-artifact relationship data (e.g. universe compatibility results)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from agent_env.config import get_config
|
|
10
|
+
from agent_env.store.document_store import Filter, UpdateSpec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnvArtifactType(str, Enum):
|
|
14
|
+
UNIVERSE_COMPATIBILITY = "universe_compatibility"
|
|
15
|
+
|
|
16
|
+
ENV_ARTIFACTS_COLLECTION = "env_artifacts"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnvArtifactStore:
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._indexed = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def _doc_store(self):
|
|
25
|
+
# Resolved per call: a cached store outlives reset_config(), so a process that
|
|
26
|
+
# re-pointed would read the new config and write the old backend.
|
|
27
|
+
store = get_config().get_document_store()
|
|
28
|
+
if self._indexed is not store:
|
|
29
|
+
store.ensure_index(
|
|
30
|
+
ENV_ARTIFACTS_COLLECTION,
|
|
31
|
+
["env_id", "env_version", "artifact_id", "artifact_version", "type"],
|
|
32
|
+
unique=True,
|
|
33
|
+
)
|
|
34
|
+
store.ensure_index(ENV_ARTIFACTS_COLLECTION, ["artifact_id", "type"])
|
|
35
|
+
self._indexed = store
|
|
36
|
+
return store
|
|
37
|
+
|
|
38
|
+
def put(self, env_id: str, env_version: int, artifact_id: str, artifact_version: int, type: str, data: dict[str, Any]) -> None:
|
|
39
|
+
filter_key = {"env_id": env_id, "env_version": env_version, "artifact_id": artifact_id, "artifact_version": artifact_version, "type": type}
|
|
40
|
+
self._doc_store.update(
|
|
41
|
+
ENV_ARTIFACTS_COLLECTION,
|
|
42
|
+
Filter.of(**filter_key),
|
|
43
|
+
UpdateSpec(set={**filter_key, "data": data, "created_at_utc": datetime.now(timezone.utc).isoformat()}),
|
|
44
|
+
upsert=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get(self, env_id: str, env_version: int, artifact_id: str, artifact_version: int, type: str) -> Optional[dict[str, Any]]:
|
|
48
|
+
return self._doc_store.find_one(
|
|
49
|
+
ENV_ARTIFACTS_COLLECTION,
|
|
50
|
+
Filter.of(env_id=env_id, env_version=env_version, artifact_id=artifact_id, artifact_version=artifact_version, type=type),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get_by_env(self, env_id: str, env_version: int | None = None, type: str | None = None) -> list[dict[str, Any]]:
|
|
54
|
+
query: dict[str, Any] = {"env_id": env_id}
|
|
55
|
+
if env_version is not None:
|
|
56
|
+
query["env_version"] = env_version
|
|
57
|
+
if type is not None:
|
|
58
|
+
query["type"] = type
|
|
59
|
+
return self._doc_store.query(ENV_ARTIFACTS_COLLECTION, Filter.of(**query))
|
|
60
|
+
|
|
61
|
+
def get_by_artifact(self, artifact_id: str, artifact_version: int | None = None, type: str | None = None) -> list[dict[str, Any]]:
|
|
62
|
+
query: dict[str, Any] = {"artifact_id": artifact_id}
|
|
63
|
+
if artifact_version is not None:
|
|
64
|
+
query["artifact_version"] = artifact_version
|
|
65
|
+
if type is not None:
|
|
66
|
+
query["type"] = type
|
|
67
|
+
return self._doc_store.query(ENV_ARTIFACTS_COLLECTION, Filter.of(**query))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_env_artifact_store: Optional[EnvArtifactStore] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_env_artifact_store() -> EnvArtifactStore:
|
|
74
|
+
global _env_artifact_store
|
|
75
|
+
if _env_artifact_store is None:
|
|
76
|
+
_env_artifact_store = EnvArtifactStore()
|
|
77
|
+
return _env_artifact_store
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def set_env_artifact_store(store: EnvArtifactStore) -> None:
|
|
81
|
+
global _env_artifact_store
|
|
82
|
+
_env_artifact_store = store
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def reset_env_artifact_store() -> None:
|
|
86
|
+
global _env_artifact_store
|
|
87
|
+
_env_artifact_store = None
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .gateway_server import GatewayEnv
|
|
2
|
+
from .mcp_server import MCPServerEnv
|
|
3
|
+
from .multi_env import MultiEnv
|
|
4
|
+
from .service_db import ServiceDBEnv
|
|
5
|
+
from .website import WebsiteEnv
|
|
6
|
+
|
|
7
|
+
__all__ = ["GatewayEnv", "MCPServerEnv", "MultiEnv", "ServiceDBEnv", "WebsiteEnv"]
|