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,1064 @@
|
|
|
1
|
+
"""Centralized configuration for agent-env.
|
|
2
|
+
|
|
3
|
+
Provides a singleton Config that manages store backends and model routing.
|
|
4
|
+
Configure once at startup, then access via get_config().
|
|
5
|
+
|
|
6
|
+
Backends resolve from ``.agentenv/config.toml`` (or ``AGENT_ENV_CONFIG``); with no
|
|
7
|
+
config, every store defaults to its local backend — no external coordinates are
|
|
8
|
+
built in:
|
|
9
|
+
|
|
10
|
+
- Document store (AGENT_ENV_DOCUMENT_STORE): "local" (or unset): stdlib SQLite at
|
|
11
|
+
document_store/documents.db under the per-user state root (``paths.state_root``).
|
|
12
|
+
MongoDB has no built-in coordinates; configure it via a [stores.document] table.
|
|
13
|
+
- Object store (AGENT_ENV_OBJECT_STORE): "local" (or unset): filesystem under
|
|
14
|
+
object_store/ in the per-user state root. S3 has no built-in coordinates; configure
|
|
15
|
+
it via a [stores.object] table.
|
|
16
|
+
- Image store (AGENT_ENV_IMAGE_STORE): "local" (or unset): an OCI registry at
|
|
17
|
+
localhost:5000. ECR has no built-in coordinates; configure it via a
|
|
18
|
+
[stores.image] table.
|
|
19
|
+
- Secret store (AGENT_ENV_SECRET_STORE): "local" (or unset): process env vars /
|
|
20
|
+
optional local file. AWS Secrets Manager has no built-in coordinates; configure
|
|
21
|
+
it via a [stores.secret] table.
|
|
22
|
+
|
|
23
|
+
AGENT_ENV_FIXTURE_PREFIX (default ""): prepends <prefix>/ to the keys of artifact
|
|
24
|
+
objects, image builds, env and agent snapshots, changelogs, default agent and judge
|
|
25
|
+
trajectories, verifier outputs and the A2A validator's fixtures, so a fresh control-plane
|
|
26
|
+
backend can write without colliding with existing objects in a shared store. Empty in prod.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import threading
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional
|
|
37
|
+
|
|
38
|
+
import yaml
|
|
39
|
+
|
|
40
|
+
from agent_env.config import loader as config_loader
|
|
41
|
+
from agent_env.config import model as model_config
|
|
42
|
+
from agent_env.config.errors import ConfigError, EmptySecretBundleError
|
|
43
|
+
from agent_env.config import snapshot as config_snapshot
|
|
44
|
+
from agent_env.config.paths import state_root
|
|
45
|
+
from agent_env.config.provenance import (
|
|
46
|
+
KIND_DEFAULT,
|
|
47
|
+
KIND_ENV,
|
|
48
|
+
KIND_FILE,
|
|
49
|
+
KIND_INSTALLED,
|
|
50
|
+
Layer,
|
|
51
|
+
Traced,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from pymongo.database import Database
|
|
56
|
+
|
|
57
|
+
from agent_env.store.document_store import DocumentStore, LocalSqliteDocumentStore
|
|
58
|
+
from agent_env.store.image_store import ImageStore
|
|
59
|
+
from agent_env.store.object_store import ObjectStore
|
|
60
|
+
from agent_env.store.secret_store import SecretStore
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
_STORE_BACKEND_MONGO = "mongo"
|
|
65
|
+
_STORE_BACKEND_LOCAL = "local"
|
|
66
|
+
|
|
67
|
+
_OBJECT_BACKEND_S3 = "s3"
|
|
68
|
+
_SECRET_BACKEND_AWS = "aws"
|
|
69
|
+
|
|
70
|
+
_RUNNER_BACKEND_LOCAL = "local"
|
|
71
|
+
|
|
72
|
+
_IMAGE_BACKEND_ECR = "ecr"
|
|
73
|
+
_DEFAULT_LOCAL_REGISTRY_HOST = "localhost:5000"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _default_sandbox_mode() -> str:
|
|
77
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_VM
|
|
78
|
+
return SANDBOX_MODE_VM
|
|
79
|
+
|
|
80
|
+
_HUMAN_A2A_CONFIG_SECTION = "conversations"
|
|
81
|
+
_HUMAN_A2A_CONFIG_KEY = "default_human_a2a_url"
|
|
82
|
+
_AGENTS_CONFIG_SECTION = "agents"
|
|
83
|
+
_AGENTS_CONFIG_KEY = "default_a2a_agent_id"
|
|
84
|
+
_DEFAULT_A2A_AGENT_ID = "a2a-default"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validated_human_a2a_url(value: object, source: str) -> str:
|
|
88
|
+
# Fail here rather than as an AttributeError or bogus URL inside a task step.
|
|
89
|
+
if not isinstance(value, str) or not value.strip():
|
|
90
|
+
raise ConfigError(f"{source} must be a non-empty string URL, got {value!r}")
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validated_a2a_agent_id(value: object, source: str) -> str:
|
|
95
|
+
if not isinstance(value, str) or not value.strip():
|
|
96
|
+
raise ConfigError(f"{source} must be a non-empty string A2A agent id, got {value!r}")
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validated_agents_section(section: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
101
|
+
"""The ``[agents]`` table, checked as the reader checks it; ``config show`` runs this too."""
|
|
102
|
+
unknown = set(section) - {_AGENTS_CONFIG_KEY}
|
|
103
|
+
if unknown:
|
|
104
|
+
raise ConfigError(
|
|
105
|
+
f"[{_AGENTS_CONFIG_SECTION}] has unknown keys {sorted(unknown)}; "
|
|
106
|
+
f"allowed: ['{_AGENTS_CONFIG_KEY}']"
|
|
107
|
+
)
|
|
108
|
+
if _AGENTS_CONFIG_KEY in section:
|
|
109
|
+
_validated_a2a_agent_id(
|
|
110
|
+
section[_AGENTS_CONFIG_KEY], f"[{_AGENTS_CONFIG_SECTION}] {_AGENTS_CONFIG_KEY}"
|
|
111
|
+
)
|
|
112
|
+
return section
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class ResolvedKey:
|
|
116
|
+
"""A single key resolved as ``env > configure() field > config.toml > built-in default``.
|
|
117
|
+
|
|
118
|
+
Declared once in ``RESOLVED_KEYS`` below and read by both the getter and
|
|
119
|
+
``agent_env.config.describe``, so a report cannot name a layer the getter would not have
|
|
120
|
+
taken — the contract ``AliasedSection`` already carries for whole sections, and the one
|
|
121
|
+
whose absence let a report claim the file while ``configure()`` was winning.
|
|
122
|
+
|
|
123
|
+
The declaration owns *precedence and coordinates* only. Which layers get validated, and
|
|
124
|
+
how, stays with the getter: those rules differ per key and are none of a report's
|
|
125
|
+
business.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
toml_path: tuple[str, ...]
|
|
129
|
+
env_var: Optional[str] = None
|
|
130
|
+
field: Optional[str] = None
|
|
131
|
+
default: Any = None
|
|
132
|
+
default_label: str = "built-in default"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def name(self) -> str:
|
|
136
|
+
return ".".join(self.toml_path)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def file_label(self) -> str:
|
|
140
|
+
"""The spelling the getters' errors use: ``[agents] default_a2a_agent_id``."""
|
|
141
|
+
return f"[{self.toml_path[0]}] {'.'.join(self.toml_path[1:])}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
RESOLVED_KEYS: tuple[ResolvedKey, ...] = (
|
|
145
|
+
ResolvedKey(("model", "api_key"), env_var="LITELLM_API_KEY"),
|
|
146
|
+
ResolvedKey(("model", "base_url"), env_var="LITELLM_BASE_URL"),
|
|
147
|
+
ResolvedKey(("conversations", "default_human_a2a_url"),
|
|
148
|
+
env_var="AGENT_ENV_HUMAN_A2A_URL", field="default_human_a2a_url"),
|
|
149
|
+
ResolvedKey(("agents", "default_a2a_agent_id"), field="default_a2a_agent_id",
|
|
150
|
+
default=_DEFAULT_A2A_AGENT_ID, default_label="built-in default in config.runtime"),
|
|
151
|
+
)
|
|
152
|
+
_KEY_BY_NAME = {key.name: key for key in RESOLVED_KEYS}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class AliasedSection:
|
|
157
|
+
"""A section resolved as ``default < config.toml < env var``, whose value may be written
|
|
158
|
+
as a name that ``alias`` expands into an impl table.
|
|
159
|
+
|
|
160
|
+
Declared once in ``ALIASED_SECTIONS`` below and read by both the resolvers and
|
|
161
|
+
``agent_env.config.describe``, so a report cannot name coordinates the resolver would
|
|
162
|
+
not have used.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
name: str
|
|
166
|
+
env_var: str
|
|
167
|
+
toml_path: tuple[str, ...]
|
|
168
|
+
default: str
|
|
169
|
+
alias: Callable[["Config", str], dict]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class PluginFailures:
|
|
173
|
+
"""What a Config's registry builds skipped, by group then name, with the reason
|
|
174
|
+
(``agent_env.plugins.load_failures``)."""
|
|
175
|
+
|
|
176
|
+
def __init__(self) -> None:
|
|
177
|
+
self._groups: dict[str, dict[str, str]] = {}
|
|
178
|
+
|
|
179
|
+
def replace(self, group: str, failures: dict[str, str]) -> None:
|
|
180
|
+
"""Record ``group``'s skipped plugins; a rebuild of the group replaces its entry."""
|
|
181
|
+
self._groups[group] = failures
|
|
182
|
+
|
|
183
|
+
def reason(self, group: str, name: str) -> Optional[str]:
|
|
184
|
+
return self._groups.get(group, {}).get(name)
|
|
185
|
+
|
|
186
|
+
def snapshot(self) -> dict[str, dict[str, str]]:
|
|
187
|
+
# Copied before reading: another thread may be recording a group it is building.
|
|
188
|
+
groups = dict(self._groups)
|
|
189
|
+
return {group: dict(failures) for group, failures in groups.items() if failures}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Identity, not field equality: a Config owns a document, so two are interchangeable only if
|
|
193
|
+
# they are the same object. Field equality called two on different config files equal.
|
|
194
|
+
@dataclass(eq=False)
|
|
195
|
+
class Config:
|
|
196
|
+
"""Configuration for storage connections."""
|
|
197
|
+
|
|
198
|
+
fixture_prefix: str = field(
|
|
199
|
+
default_factory=lambda: os.getenv("AGENT_ENV_FIXTURE_PREFIX", "")
|
|
200
|
+
)
|
|
201
|
+
agent_sandbox_mode: str = field(
|
|
202
|
+
default_factory=lambda: os.getenv("AGENT_SANDBOX_MODE") or _default_sandbox_mode()
|
|
203
|
+
)
|
|
204
|
+
default_gateway_env_id: str = "default"
|
|
205
|
+
default_service_db_env_id: str = "default-db"
|
|
206
|
+
default_website_browser_env_id: str = "website-browser"
|
|
207
|
+
default_a2a_agent_id: Optional[str] = None
|
|
208
|
+
modal_default_region: str = field(
|
|
209
|
+
default_factory=lambda: os.getenv("AGENT_ENV_MODAL_REGION", "us-east-1")
|
|
210
|
+
)
|
|
211
|
+
default_human_a2a_url: Optional[str] = None
|
|
212
|
+
|
|
213
|
+
_secret: Optional[Mapping[str, Any]] = field(default=None, repr=False, compare=False)
|
|
214
|
+
_snapshot: Optional[config_snapshot.Snapshot] = field(default=None, repr=False, compare=False)
|
|
215
|
+
_snapshot_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
|
|
216
|
+
_document_store: Optional["DocumentStore"] = field(default=None, repr=False, compare=False)
|
|
217
|
+
_document_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
|
|
218
|
+
_image_store: Optional[ImageStore] = field(default=None, repr=False, compare=False)
|
|
219
|
+
_image_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
|
|
220
|
+
_object_store: Optional[ObjectStore] = field(default=None, repr=False, compare=False)
|
|
221
|
+
_object_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
|
|
222
|
+
_local_stores: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
223
|
+
_routed_stores: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
224
|
+
_separate_stores: dict[str, tuple[Any, Any]] = field(default_factory=dict, repr=False, compare=False)
|
|
225
|
+
_routing_lock: Any = field(default_factory=threading.RLock, repr=False, compare=False)
|
|
226
|
+
_secret_store: Optional[SecretStore] = field(default=None, repr=False, compare=False)
|
|
227
|
+
_secret_store_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
|
|
228
|
+
_model_cfg: Optional[model_config.ModelConfig] = field(default=None, repr=False, compare=False)
|
|
229
|
+
_runner: Optional[Any] = field(default=None, repr=False, compare=False)
|
|
230
|
+
_runner_lock: Any = field(default_factory=threading.Lock, repr=False, compare=False)
|
|
231
|
+
_registries: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
232
|
+
_plugin_failures: PluginFailures = field(default_factory=PluginFailures, repr=False, compare=False)
|
|
233
|
+
|
|
234
|
+
def _registry(self, name: str, build: Callable[..., Any]) -> Any:
|
|
235
|
+
"""One registry, memoized against this Config's document.
|
|
236
|
+
|
|
237
|
+
Unlocked unlike the stores: ``build`` runs ``load_impl``, and racing builds read the
|
|
238
|
+
same pinned document anyway.
|
|
239
|
+
"""
|
|
240
|
+
cached = self._registries.get(name)
|
|
241
|
+
if cached is None:
|
|
242
|
+
# Pinned before ``build`` imports any plugin: a plugin package that points
|
|
243
|
+
# AGENT_ENV_CONFIG elsewhere on import must not change which config this one reads.
|
|
244
|
+
self._document()
|
|
245
|
+
state = _registries_building.__dict__
|
|
246
|
+
building = state.setdefault("names", set())
|
|
247
|
+
if name in building:
|
|
248
|
+
# Re-entered while this thread is still building that registry (a plugin asked
|
|
249
|
+
# for it while being imported), so it may lack that plugin: returned, not kept,
|
|
250
|
+
# and its failures unrecorded, since the outer build records the whole group.
|
|
251
|
+
state["nested"] = state.get("nested", 0) + 1
|
|
252
|
+
try:
|
|
253
|
+
return build(self)
|
|
254
|
+
finally:
|
|
255
|
+
state["nested"] -= 1
|
|
256
|
+
building.add(name)
|
|
257
|
+
# A registry first built inside a re-entered one is kept, so it records its own failures.
|
|
258
|
+
outer, state["nested"] = state.get("nested", 0), 0
|
|
259
|
+
try:
|
|
260
|
+
cached = self._registries[name] = build(self)
|
|
261
|
+
finally:
|
|
262
|
+
building.discard(name)
|
|
263
|
+
state["nested"] = outer
|
|
264
|
+
return cached
|
|
265
|
+
|
|
266
|
+
# Imported per method: circular at module scope, and they drag in the model classes.
|
|
267
|
+
def env_registry(self) -> dict[str, Any]:
|
|
268
|
+
from agent_env.env import registry
|
|
269
|
+
|
|
270
|
+
return self._registry("envs", registry._build_registry)
|
|
271
|
+
|
|
272
|
+
def artifact_registry(self) -> dict[str, Any]:
|
|
273
|
+
from agent_env.artifact import registry
|
|
274
|
+
|
|
275
|
+
return self._registry("artifacts", registry._build_registry)
|
|
276
|
+
|
|
277
|
+
def artifact_type_aliases(self) -> dict[str, str]:
|
|
278
|
+
from agent_env.artifact import registry
|
|
279
|
+
|
|
280
|
+
return self._registry("artifact_type_aliases", registry._load_type_aliases)
|
|
281
|
+
|
|
282
|
+
def task_step_registry(self) -> dict[str, Any]:
|
|
283
|
+
from agent_env.task_step import registry
|
|
284
|
+
|
|
285
|
+
return self._registry("task_steps", registry._build_registry)
|
|
286
|
+
|
|
287
|
+
def sandbox_registry(self) -> dict[str, dict]:
|
|
288
|
+
from agent_env.providers.sandbox_providers import sandbox_provider
|
|
289
|
+
|
|
290
|
+
return self._registry("sandbox", sandbox_provider._build_registry)
|
|
291
|
+
|
|
292
|
+
def state_registry(self) -> dict[str, dict]:
|
|
293
|
+
from agent_env.providers.env_state import env_state_provider
|
|
294
|
+
|
|
295
|
+
return self._registry("state", env_state_provider._build_registry)
|
|
296
|
+
|
|
297
|
+
def env_provider_registry(self) -> dict[str, Any]:
|
|
298
|
+
from agent_env.providers.env_providers import env_provider
|
|
299
|
+
|
|
300
|
+
return self._registry("env_providers", env_provider._build_registry)
|
|
301
|
+
|
|
302
|
+
def _get_secret(self) -> Mapping[str, Any]:
|
|
303
|
+
"""Combined secret mapping from the configured secret store — the whole-bundle reader
|
|
304
|
+
for callers that index individual keys, sharing ``get_secret_store()`` with ``secret:``
|
|
305
|
+
interpolation (so they can't diverge). Needs a bundle-backed store."""
|
|
306
|
+
if self._secret is None:
|
|
307
|
+
store = self.get_secret_store()
|
|
308
|
+
load = getattr(store, "_load", None)
|
|
309
|
+
if not callable(load):
|
|
310
|
+
raise ConfigError(
|
|
311
|
+
f"{type(store).__name__} exposes no combined secret mapping; "
|
|
312
|
+
"_get_secret() needs a bundle-backed secret store (aws/local)."
|
|
313
|
+
)
|
|
314
|
+
bundle = load()
|
|
315
|
+
if not bundle:
|
|
316
|
+
raise EmptySecretBundleError(
|
|
317
|
+
"Secret bundle is empty: configure a [stores.secret] table (or point "
|
|
318
|
+
"AGENT_ENV_CONFIG at a config.toml with one), or populate the local "
|
|
319
|
+
"secret store's file/values."
|
|
320
|
+
)
|
|
321
|
+
self._secret = bundle
|
|
322
|
+
return self._secret
|
|
323
|
+
|
|
324
|
+
def get_litellm_api_key(self) -> str:
|
|
325
|
+
return self._model_config().resolve_api_key(secret_resolver=self._resolve_secret)
|
|
326
|
+
|
|
327
|
+
def get_modal_credentials(self) -> tuple[str, str]:
|
|
328
|
+
token_id = os.getenv("MODAL_TOKEN_ID")
|
|
329
|
+
token_secret = os.getenv("MODAL_TOKEN_SECRET")
|
|
330
|
+
if token_id and token_secret:
|
|
331
|
+
return token_id, token_secret
|
|
332
|
+
|
|
333
|
+
modal_toml = Path.home() / ".modal.toml"
|
|
334
|
+
if modal_toml.exists():
|
|
335
|
+
import tomllib
|
|
336
|
+
for profile in tomllib.loads(modal_toml.read_text()).values():
|
|
337
|
+
if isinstance(profile, dict) and profile.get("active") and profile.get("token_id") and profile.get("token_secret"):
|
|
338
|
+
return profile["token_id"], profile["token_secret"]
|
|
339
|
+
|
|
340
|
+
secret = self._get_secret()
|
|
341
|
+
return secret["modal_token_id"], secret["modal_token_secret"]
|
|
342
|
+
|
|
343
|
+
def get_litellm_base_url(self) -> str:
|
|
344
|
+
base = os.getenv("LITELLM_BASE_URL") or self._model_config().base_url
|
|
345
|
+
if not base:
|
|
346
|
+
raise ConfigError(
|
|
347
|
+
"No model endpoint configured: set [model] base_url in "
|
|
348
|
+
".agentenv/config.toml or the LITELLM_BASE_URL env var."
|
|
349
|
+
)
|
|
350
|
+
return base
|
|
351
|
+
|
|
352
|
+
def _model_config(self) -> model_config.ModelConfig:
|
|
353
|
+
"""The parsed ``[model]`` config, cached."""
|
|
354
|
+
if self._model_cfg is None:
|
|
355
|
+
self._model_cfg = model_config.ModelConfig.from_section(
|
|
356
|
+
self.section("model"), secret_resolver=self._resolve_secret
|
|
357
|
+
)
|
|
358
|
+
return self._model_cfg
|
|
359
|
+
|
|
360
|
+
def get_default_human_a2a_url(self) -> str:
|
|
361
|
+
"""The base URL a human-A2A (HITL) step parks its conversation on.
|
|
362
|
+
|
|
363
|
+
First match wins: ``AGENT_ENV_HUMAN_A2A_URL`` (absolute — local dev repoints
|
|
364
|
+
it at the local hub), an explicit ``configure(default_human_a2a_url=...)``,
|
|
365
|
+
then config.toml ``[conversations] default_human_a2a_url``; unconfigured
|
|
366
|
+
resolution raises, so call this only where the URL is consumed. Returns the
|
|
367
|
+
*base* URL; ``deploy_human_agent`` appends ``/instance/{instance_id}``.
|
|
368
|
+
"""
|
|
369
|
+
key = _KEY_BY_NAME[f"{_HUMAN_A2A_CONFIG_SECTION}.{_HUMAN_A2A_CONFIG_KEY}"]
|
|
370
|
+
for layer in self.key_candidates(key):
|
|
371
|
+
if layer.kind == KIND_ENV:
|
|
372
|
+
if layer.raw:
|
|
373
|
+
return layer.raw # deliberately unvalidated, as before
|
|
374
|
+
continue
|
|
375
|
+
if layer.kind == KIND_INSTALLED:
|
|
376
|
+
return _validated_human_a2a_url(layer.raw, key.field)
|
|
377
|
+
# Only here, where the file is what we are about to take, does its shape matter.
|
|
378
|
+
# Checking it earlier lets a malformed [conversations] beat an override that was
|
|
379
|
+
# never going to read the file at all.
|
|
380
|
+
self.section(_HUMAN_A2A_CONFIG_SECTION)
|
|
381
|
+
resolved = config_loader.interpolate(layer.raw, secret_resolver=self._resolve_secret)
|
|
382
|
+
return _validated_human_a2a_url(resolved, key.file_label)
|
|
383
|
+
raise ConfigError(f"No human-A2A URL configured: set [{_HUMAN_A2A_CONFIG_SECTION}] {_HUMAN_A2A_CONFIG_KEY} or AGENT_ENV_HUMAN_A2A_URL")
|
|
384
|
+
|
|
385
|
+
def get_default_a2a_agent_id(self) -> str:
|
|
386
|
+
"""The agent a ``deploy_agent`` step without an id deploys:
|
|
387
|
+
``configure(default_a2a_agent_id=...)`` > ``[agents] default_a2a_agent_id`` > ``a2a-default``."""
|
|
388
|
+
key = _KEY_BY_NAME[f"{_AGENTS_CONFIG_SECTION}.{_AGENTS_CONFIG_KEY}"]
|
|
389
|
+
for layer in self.key_candidates(key):
|
|
390
|
+
if layer.kind == KIND_INSTALLED:
|
|
391
|
+
return _validated_a2a_agent_id(layer.raw, key.field)
|
|
392
|
+
# The file is in play now — or the built-in default beneath it, which the old
|
|
393
|
+
# code also validated the section before returning. Not before: a broken table
|
|
394
|
+
# must not beat configure().
|
|
395
|
+
validated_agents_section(self.section(_AGENTS_CONFIG_SECTION))
|
|
396
|
+
if layer.kind == KIND_DEFAULT:
|
|
397
|
+
return layer.raw
|
|
398
|
+
resolved = config_loader.interpolate(layer.raw, secret_resolver=self._resolve_secret)
|
|
399
|
+
return _validated_a2a_agent_id(resolved, key.file_label)
|
|
400
|
+
return _DEFAULT_A2A_AGENT_ID
|
|
401
|
+
|
|
402
|
+
def get_default_model(self) -> str | None:
|
|
403
|
+
return self._model_config().default
|
|
404
|
+
|
|
405
|
+
def get_model_for_role(self, role: str) -> str | None:
|
|
406
|
+
return self._model_config().model_for_role(role)
|
|
407
|
+
|
|
408
|
+
def get_model_params(self, overrides: dict | None = None) -> dict:
|
|
409
|
+
params = dict(self._model_config().params)
|
|
410
|
+
if overrides:
|
|
411
|
+
params.update(config_loader.interpolate(overrides, secret_resolver=self._resolve_secret))
|
|
412
|
+
return params
|
|
413
|
+
|
|
414
|
+
def resolve_model_call(
|
|
415
|
+
self,
|
|
416
|
+
model: str,
|
|
417
|
+
*,
|
|
418
|
+
default_api_key: str | None = None,
|
|
419
|
+
base_override: str | None = None,
|
|
420
|
+
model_overrides: dict[str, dict[str, str]] | None = None,
|
|
421
|
+
) -> model_config.ModelCallConfig:
|
|
422
|
+
return self._model_config().resolve_call(
|
|
423
|
+
model,
|
|
424
|
+
default_api_key=default_api_key,
|
|
425
|
+
base_override=base_override,
|
|
426
|
+
model_overrides=model_overrides,
|
|
427
|
+
secret_resolver=self._resolve_secret,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def db(self) -> "Database":
|
|
432
|
+
"""Raw pymongo Database shared with the configured Mongo document store, for callers
|
|
433
|
+
that need collections the DocumentStore API does not model."""
|
|
434
|
+
from agent_env.store.document_store import MongoDocumentStore
|
|
435
|
+
|
|
436
|
+
store = self._configured_document_store()
|
|
437
|
+
if not isinstance(store, MongoDocumentStore):
|
|
438
|
+
raise ConfigError(
|
|
439
|
+
"Config.db needs a Mongo document store, but the configured store is "
|
|
440
|
+
f"{type(store).__name__}: configure a [stores.document] table with a "
|
|
441
|
+
"MongoDocumentStore impl (and unset AGENT_ENV_DOCUMENT_STORE, which "
|
|
442
|
+
"overrides it)."
|
|
443
|
+
)
|
|
444
|
+
return store.database
|
|
445
|
+
|
|
446
|
+
def get_document_store(self) -> "DocumentStore":
|
|
447
|
+
"""The control-plane document store (cached): an explicitly-set backend, else the one from
|
|
448
|
+
``AGENT_ENV_DOCUMENT_STORE`` / config.toml (``local`` → SQLite default). With namespace
|
|
449
|
+
routing on, ``@local`` documents and ``@local`` runs' records go to the ``@local`` namespace's
|
|
450
|
+
own local store (``agent_env.store.routing``)."""
|
|
451
|
+
from agent_env.store import routing
|
|
452
|
+
|
|
453
|
+
configured = self._configured_document_store()
|
|
454
|
+
if not routing.namespace_routing_enabled():
|
|
455
|
+
return configured
|
|
456
|
+
return self._routed("document", configured)
|
|
457
|
+
|
|
458
|
+
def _configured_document_store(self) -> "DocumentStore":
|
|
459
|
+
if self._document_store is None:
|
|
460
|
+
with self._document_store_lock:
|
|
461
|
+
if self._document_store is None:
|
|
462
|
+
self._document_store = self._build_default_document_store()
|
|
463
|
+
return self._document_store
|
|
464
|
+
|
|
465
|
+
def set_document_store(self, store: "DocumentStore") -> None:
|
|
466
|
+
"""Install an explicit backend; get_document_store() then returns it verbatim."""
|
|
467
|
+
self._document_store = store
|
|
468
|
+
|
|
469
|
+
def _build_default_document_store(self) -> DocumentStore:
|
|
470
|
+
from agent_env.store.document_store import DocumentStore
|
|
471
|
+
|
|
472
|
+
section = self._resolve_document_section()
|
|
473
|
+
return config_loader.build_store(section, DocumentStore, secret_resolver=self._resolve_secret)
|
|
474
|
+
|
|
475
|
+
def trace_section(self, name: str) -> Traced:
|
|
476
|
+
"""A declared section's value plus every layer that could have supplied it — winner
|
|
477
|
+
first, later layer wins: default < config.toml < env var.
|
|
478
|
+
|
|
479
|
+
A string (an env value or a bare toml value) expands through the section's alias; a
|
|
480
|
+
table is used as-is. Reporting reads the same layers, so both the precedence rule and
|
|
481
|
+
the section's coordinates are stated exactly once.
|
|
482
|
+
"""
|
|
483
|
+
section = _BY_NAME[name]
|
|
484
|
+
candidates = self.section_candidates(name)
|
|
485
|
+
if candidates[0].error is not None:
|
|
486
|
+
raise ConfigError(candidates[0].error)
|
|
487
|
+
raw = candidates[0].raw
|
|
488
|
+
return Traced(
|
|
489
|
+
value=section.alias(self, raw) if isinstance(raw, str) else raw,
|
|
490
|
+
winner=candidates[0],
|
|
491
|
+
shadowed=tuple(candidates[1:]),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def key_candidates(self, key: "ResolvedKey") -> list[Layer]:
|
|
495
|
+
"""Every layer that could supply ``key``, highest priority first.
|
|
496
|
+
|
|
497
|
+
Separate from the getters so the report reads the same order they take, rather than
|
|
498
|
+
a second copy of it that can drift.
|
|
499
|
+
"""
|
|
500
|
+
candidates: list[Layer] = []
|
|
501
|
+
if key.env_var is not None:
|
|
502
|
+
override = os.getenv(key.env_var)
|
|
503
|
+
if override is not None:
|
|
504
|
+
candidates.append(Layer(KIND_ENV, f"${key.env_var}", override))
|
|
505
|
+
if key.field is not None:
|
|
506
|
+
installed = getattr(self, key.field, None)
|
|
507
|
+
if installed is not None:
|
|
508
|
+
candidates.append(Layer(KIND_INSTALLED, f"configure({key.field}=...)", installed))
|
|
509
|
+
from_file = self.file_candidate(key.toml_path)
|
|
510
|
+
if from_file is not None:
|
|
511
|
+
candidates.append(from_file)
|
|
512
|
+
if key.default is not None:
|
|
513
|
+
candidates.append(Layer(KIND_DEFAULT, key.default_label, key.default))
|
|
514
|
+
return candidates
|
|
515
|
+
|
|
516
|
+
def section_candidates(self, name: str) -> list[Layer]:
|
|
517
|
+
"""Every layer that could supply a declared section, highest priority first.
|
|
518
|
+
|
|
519
|
+
Separate from resolution so a report can still name the layer at fault when turning
|
|
520
|
+
that layer's value into a section is what failed.
|
|
521
|
+
"""
|
|
522
|
+
section = _BY_NAME[name]
|
|
523
|
+
candidates: list[Layer] = []
|
|
524
|
+
override = os.getenv(section.env_var)
|
|
525
|
+
if override is not None:
|
|
526
|
+
candidates.append(Layer(KIND_ENV, f"${section.env_var}", override))
|
|
527
|
+
from_file = self.file_candidate(section.toml_path)
|
|
528
|
+
if from_file is not None:
|
|
529
|
+
candidates.append(from_file)
|
|
530
|
+
candidates.append(Layer(KIND_DEFAULT, "built-in default", section.default))
|
|
531
|
+
return candidates
|
|
532
|
+
|
|
533
|
+
def file_candidate(self, toml_path: tuple[str, ...]) -> Optional[Layer]:
|
|
534
|
+
"""The file's contribution to this section, or a candidate carrying why it could not
|
|
535
|
+
be read. Never raises: a layer that something outranks must not be able to break the
|
|
536
|
+
layer that outranks it — `trace_section` raises only if this one wins.
|
|
537
|
+
"""
|
|
538
|
+
label = f"[{'.'.join(toml_path)}]"
|
|
539
|
+
try:
|
|
540
|
+
node: Any = self.config_file()
|
|
541
|
+
except (ConfigError, OSError) as e:
|
|
542
|
+
return Layer(KIND_FILE, label, None, error=str(e))
|
|
543
|
+
walked: list[str] = []
|
|
544
|
+
for part in toml_path:
|
|
545
|
+
if not isinstance(node, dict):
|
|
546
|
+
return Layer(KIND_FILE, label, None, error=(
|
|
547
|
+
f"config.toml [{'.'.join(walked)}] must be a table, got {type(node).__name__}"
|
|
548
|
+
))
|
|
549
|
+
walked.append(part)
|
|
550
|
+
node = node.get(part)
|
|
551
|
+
if node is None:
|
|
552
|
+
return None
|
|
553
|
+
return Layer(KIND_FILE, label, node)
|
|
554
|
+
|
|
555
|
+
def _resolve_document_section(self) -> dict:
|
|
556
|
+
return self.trace_section("document").value
|
|
557
|
+
|
|
558
|
+
def _document_alias(self, name: str) -> dict:
|
|
559
|
+
if name == _STORE_BACKEND_MONGO:
|
|
560
|
+
raise ConfigError(
|
|
561
|
+
"The 'mongo' document backend has no built-in coordinates: configure a "
|
|
562
|
+
"[stores.document] table (and unset AGENT_ENV_DOCUMENT_STORE, which "
|
|
563
|
+
"overrides it)."
|
|
564
|
+
)
|
|
565
|
+
if name == _STORE_BACKEND_LOCAL:
|
|
566
|
+
return {
|
|
567
|
+
"impl": "agent_env.store.document_store:LocalSqliteDocumentStore",
|
|
568
|
+
"config": {"path": self._local_document_store_path()},
|
|
569
|
+
}
|
|
570
|
+
raise ConfigError(
|
|
571
|
+
f"Unknown AGENT_ENV_DOCUMENT_STORE={name!r} (expected 'local', or a [stores.document] table for mongo)"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def _local_document_store_path(self) -> str:
|
|
575
|
+
return str(state_root() / "document_store" / "documents.db")
|
|
576
|
+
|
|
577
|
+
def local_namespace_document_store(self) -> "LocalSqliteDocumentStore":
|
|
578
|
+
"""The ``@local`` namespace's own document store, whether or not routing is on. Records about
|
|
579
|
+
``@local`` entities, like the bundle ledger, live beside them here and never reach a configured
|
|
580
|
+
store. Building it creates nothing; its first write creates the file."""
|
|
581
|
+
return self._local_store("document")
|
|
582
|
+
|
|
583
|
+
def _local_namespace_document_store_path(self) -> str:
|
|
584
|
+
return str(state_root() / "document_store" / "local.db")
|
|
585
|
+
|
|
586
|
+
def _document(self) -> config_snapshot.Snapshot:
|
|
587
|
+
"""The document this Config reads, resolved on first use and kept.
|
|
588
|
+
|
|
589
|
+
Lazy, not in ``__init__``: ``config show`` builds a Config to *report* a broken
|
|
590
|
+
``AGENT_ENV_CONFIG``, and a raising constructor would take that report away.
|
|
591
|
+
"""
|
|
592
|
+
if self._snapshot is None:
|
|
593
|
+
with self._snapshot_lock:
|
|
594
|
+
if self._snapshot is None:
|
|
595
|
+
self._snapshot = config_snapshot.resolve()
|
|
596
|
+
return self._snapshot
|
|
597
|
+
|
|
598
|
+
def section(self, *names: str) -> Mapping[str, Any]:
|
|
599
|
+
"""The config table at ``names``: ``{}`` when absent, an error when mis-shaped."""
|
|
600
|
+
return self._document().section(*names)
|
|
601
|
+
|
|
602
|
+
def config_path(self) -> Optional[Path]:
|
|
603
|
+
"""The config.toml this Config resolves against. The process resolves one document,
|
|
604
|
+
so every section and every local-store path agree about which file they came from."""
|
|
605
|
+
return self._document().path
|
|
606
|
+
|
|
607
|
+
def config_file(self) -> dict:
|
|
608
|
+
"""The parsed config.toml (``{}`` when absent) — this Config's one resolution.
|
|
609
|
+
|
|
610
|
+
Raises if that resolution failed to parse. The path stays available through
|
|
611
|
+
``config_path`` either way, which is what lets a report name a file it cannot read.
|
|
612
|
+
"""
|
|
613
|
+
return dict(self._document().section())
|
|
614
|
+
|
|
615
|
+
def get_object_store(self) -> ObjectStore:
|
|
616
|
+
"""The object (blob) store (cached): an explicitly-set backend, else the one from
|
|
617
|
+
``AGENT_ENV_OBJECT_STORE`` / config.toml (``local`` → filesystem default). While an ``@local``
|
|
618
|
+
task runs under namespace routing, its view of it keeps writes local."""
|
|
619
|
+
from agent_env.store import routing
|
|
620
|
+
|
|
621
|
+
configured = self._configured_object_store()
|
|
622
|
+
if not routing.in_local_run():
|
|
623
|
+
return configured
|
|
624
|
+
return self._routed("object", configured)
|
|
625
|
+
|
|
626
|
+
def get_object_store_for(self, entity_id: str) -> ObjectStore:
|
|
627
|
+
"""The object store ``entity_id``'s objects are written to: under namespace routing, the
|
|
628
|
+
per-user local one for an ``@local`` id; otherwise the configured one."""
|
|
629
|
+
from agent_env.store import routing
|
|
630
|
+
from agent_env.store.ids import is_local_id
|
|
631
|
+
|
|
632
|
+
configured, local = self._namespace_stores("object")
|
|
633
|
+
if local is None:
|
|
634
|
+
return configured
|
|
635
|
+
if is_local_id(entity_id):
|
|
636
|
+
return local
|
|
637
|
+
routing.refuse_in_local_run(f"writing objects for {entity_id!r}")
|
|
638
|
+
return configured
|
|
639
|
+
|
|
640
|
+
def get_object_store_at(self, object_url: str) -> ObjectStore:
|
|
641
|
+
"""The object store ``object_url`` addresses: under namespace routing a url the local store
|
|
642
|
+
owns is the local store's, unless the configured store owns it too."""
|
|
643
|
+
from agent_env.store import routing
|
|
644
|
+
|
|
645
|
+
configured, local = self._namespace_stores("object")
|
|
646
|
+
return configured if local is None else routing.object_store_holding(object_url, configured, local)
|
|
647
|
+
|
|
648
|
+
def get_object_store_to_write(self, object_url: str, entity_id: str) -> ObjectStore:
|
|
649
|
+
"""``get_object_store_at`` for writing, or registering, ``entity_id``'s object at ``object_url``."""
|
|
650
|
+
self.check_object_url(entity_id, object_url)
|
|
651
|
+
return self.get_object_store_at(object_url)
|
|
652
|
+
|
|
653
|
+
def check_object_url(self, entity_id: str, object_url: str) -> None:
|
|
654
|
+
"""Refuse recording ``object_url`` as ``entity_id``'s object across namespaces: under namespace
|
|
655
|
+
routing an ``@local`` entity's objects live in the local store, a bare one's in the configured
|
|
656
|
+
store, and an ``@local`` task run writes only locally."""
|
|
657
|
+
from agent_env.store import routing
|
|
658
|
+
from agent_env.store.ids import is_local_id
|
|
659
|
+
|
|
660
|
+
configured, local = self._namespace_stores("object")
|
|
661
|
+
if local is None:
|
|
662
|
+
return
|
|
663
|
+
store = routing.object_store_holding(object_url, configured, local)
|
|
664
|
+
if is_local_id(entity_id) and store is not local:
|
|
665
|
+
raise ValueError(f"{entity_id!r} is an @local id, so its objects go to the local object store, not {object_url!r}")
|
|
666
|
+
if not is_local_id(entity_id) and store is local:
|
|
667
|
+
raise ValueError(f"{entity_id!r} is not an @local id, so its objects go to the configured object store, not {object_url!r}")
|
|
668
|
+
if store is configured:
|
|
669
|
+
routing.refuse_in_local_run(f"a write to {object_url!r}")
|
|
670
|
+
|
|
671
|
+
def check_local_run_write(self, entity_id: str) -> None:
|
|
672
|
+
"""Refuse writing bare-id ``entity_id`` while an ``@local`` task runs where a configured store
|
|
673
|
+
isn't the per-user one: the write would land in a shared store."""
|
|
674
|
+
from agent_env.store import routing
|
|
675
|
+
from agent_env.store.ids import is_local_id
|
|
676
|
+
|
|
677
|
+
if not routing.in_local_run() or is_local_id(entity_id):
|
|
678
|
+
return
|
|
679
|
+
if any(self._namespace_stores(kind)[1] is not None for kind in ("document", "object", "image")):
|
|
680
|
+
routing.refuse_in_local_run(f"writing {entity_id!r}")
|
|
681
|
+
|
|
682
|
+
def _configured_object_store(self) -> ObjectStore:
|
|
683
|
+
if self._object_store is None:
|
|
684
|
+
with self._object_store_lock:
|
|
685
|
+
if self._object_store is None:
|
|
686
|
+
self._object_store = self._build_default_object_store()
|
|
687
|
+
return self._object_store
|
|
688
|
+
|
|
689
|
+
def set_object_store(self, store: ObjectStore) -> None:
|
|
690
|
+
"""Install an explicit backend; get_object_store() then returns it verbatim."""
|
|
691
|
+
self._object_store = store
|
|
692
|
+
|
|
693
|
+
def _build_default_object_store(self) -> ObjectStore:
|
|
694
|
+
from agent_env.store.object_store import ObjectStore
|
|
695
|
+
|
|
696
|
+
section = self._resolve_object_section()
|
|
697
|
+
return config_loader.build_store(section, ObjectStore, secret_resolver=self._resolve_secret)
|
|
698
|
+
|
|
699
|
+
def _resolve_object_section(self) -> dict:
|
|
700
|
+
return self.trace_section("object").value
|
|
701
|
+
|
|
702
|
+
def _object_alias(self, name: str) -> dict:
|
|
703
|
+
if name == _OBJECT_BACKEND_S3:
|
|
704
|
+
raise ConfigError(
|
|
705
|
+
"The 's3' object backend has no built-in coordinates: configure a "
|
|
706
|
+
"[stores.object] table (and unset AGENT_ENV_OBJECT_STORE, which overrides it)."
|
|
707
|
+
)
|
|
708
|
+
if name == _STORE_BACKEND_LOCAL:
|
|
709
|
+
return {
|
|
710
|
+
"impl": "agent_env.store.object_store:LocalFilesystemObjectStore",
|
|
711
|
+
"config": {"root": self._local_object_store_path()},
|
|
712
|
+
}
|
|
713
|
+
raise ConfigError(
|
|
714
|
+
f"Unknown AGENT_ENV_OBJECT_STORE={name!r} (expected 'local', or a [stores.object] table for a hosted backend)"
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def _local_object_store_path(self) -> str:
|
|
718
|
+
return str(state_root() / "object_store")
|
|
719
|
+
|
|
720
|
+
def get_image_store(self) -> ImageStore:
|
|
721
|
+
"""The image (registry) store (cached): an explicitly-set backend, else the one from
|
|
722
|
+
``AGENT_ENV_IMAGE_STORE`` / config.toml (``local`` → local registry default). While an
|
|
723
|
+
``@local`` task runs under namespace routing, its view of it keeps pushes local."""
|
|
724
|
+
from agent_env.store import routing
|
|
725
|
+
|
|
726
|
+
configured = self._configured_image_store()
|
|
727
|
+
if not routing.in_local_run():
|
|
728
|
+
return configured
|
|
729
|
+
return self._routed("image", configured)
|
|
730
|
+
|
|
731
|
+
def get_image_store_for(self, entity_id: str) -> ImageStore:
|
|
732
|
+
"""The image store ``entity_id``'s image is pushed to: under namespace routing, the local
|
|
733
|
+
registry for an ``@local`` id; otherwise the configured one."""
|
|
734
|
+
from agent_env.store import routing
|
|
735
|
+
from agent_env.store.ids import is_local_id
|
|
736
|
+
|
|
737
|
+
configured, local = self._namespace_stores("image")
|
|
738
|
+
if local is None:
|
|
739
|
+
return configured
|
|
740
|
+
if is_local_id(entity_id):
|
|
741
|
+
return local
|
|
742
|
+
routing.refuse_in_local_run(f"pushing an image for {entity_id!r}")
|
|
743
|
+
return configured
|
|
744
|
+
|
|
745
|
+
def get_image_store_at(self, ref: str) -> ImageStore:
|
|
746
|
+
"""The image store serving ``ref``: under namespace routing, the local registry for a ref on
|
|
747
|
+
its host; otherwise the configured one."""
|
|
748
|
+
from agent_env.store import routing
|
|
749
|
+
|
|
750
|
+
configured, local = self._namespace_stores("image")
|
|
751
|
+
return configured if local is None else routing.image_store_holding(ref, configured, local)
|
|
752
|
+
|
|
753
|
+
def _configured_image_store(self) -> ImageStore:
|
|
754
|
+
if self._image_store is None:
|
|
755
|
+
with self._image_store_lock:
|
|
756
|
+
if self._image_store is None:
|
|
757
|
+
self._image_store = self._build_default_image_store()
|
|
758
|
+
return self._image_store
|
|
759
|
+
|
|
760
|
+
def set_image_store(self, store: ImageStore) -> None:
|
|
761
|
+
"""Install an explicit backend; get_image_store() then returns it verbatim."""
|
|
762
|
+
self._image_store = store
|
|
763
|
+
|
|
764
|
+
def _build_default_image_store(self) -> ImageStore:
|
|
765
|
+
from agent_env.store.image_store import ImageStore
|
|
766
|
+
|
|
767
|
+
section = self._resolve_image_section()
|
|
768
|
+
return config_loader.build_store(section, ImageStore, secret_resolver=self._resolve_secret)
|
|
769
|
+
|
|
770
|
+
def _resolve_image_section(self) -> dict:
|
|
771
|
+
return self.trace_section("image").value
|
|
772
|
+
|
|
773
|
+
def _image_alias(self, name: str) -> dict:
|
|
774
|
+
if name == _IMAGE_BACKEND_ECR:
|
|
775
|
+
raise ConfigError(
|
|
776
|
+
"The 'ecr' image backend has no built-in coordinates: configure a "
|
|
777
|
+
"[stores.image] table (and unset AGENT_ENV_IMAGE_STORE, which overrides it)."
|
|
778
|
+
)
|
|
779
|
+
if name == _STORE_BACKEND_LOCAL:
|
|
780
|
+
return {
|
|
781
|
+
"impl": "agent_env.store.image_store:LocalRegistryImageStore",
|
|
782
|
+
"config": {"registry_host": _DEFAULT_LOCAL_REGISTRY_HOST},
|
|
783
|
+
}
|
|
784
|
+
raise ConfigError(
|
|
785
|
+
f"Unknown AGENT_ENV_IMAGE_STORE={name!r} (expected 'local', or a [stores.image] table for ecr)"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
def _local_store(self, kind: str) -> Any:
|
|
789
|
+
"""The store of ``kind`` the ``@local`` namespace lives in. Documents get a file of their own
|
|
790
|
+
beside the default local store's, so neither ever holds the other's documents; objects and
|
|
791
|
+
images use the per-user object store and local registry, whose urls and repositories already
|
|
792
|
+
say whose they are."""
|
|
793
|
+
from agent_env.store.document_store import DocumentStore
|
|
794
|
+
from agent_env.store.image_store import ImageStore
|
|
795
|
+
from agent_env.store.object_store import ObjectStore
|
|
796
|
+
|
|
797
|
+
with self._routing_lock:
|
|
798
|
+
if kind not in self._local_stores:
|
|
799
|
+
if kind == "document":
|
|
800
|
+
section, abc = {
|
|
801
|
+
"impl": "agent_env.store.routing:LocalNamespaceDocumentStore",
|
|
802
|
+
"config": {"path": self._local_namespace_document_store_path()},
|
|
803
|
+
}, DocumentStore
|
|
804
|
+
else:
|
|
805
|
+
alias, abc = {"object": (self._object_alias, ObjectStore), "image": (self._image_alias, ImageStore)}[kind]
|
|
806
|
+
section = alias(_STORE_BACKEND_LOCAL)
|
|
807
|
+
self._local_stores[kind] = config_loader.build_store(section, abc, secret_resolver=self._resolve_secret)
|
|
808
|
+
return self._local_stores[kind]
|
|
809
|
+
|
|
810
|
+
def _namespace_stores(self, kind: str) -> tuple[Any, Any]:
|
|
811
|
+
"""The configured store of ``kind`` and, under namespace routing, the ``@local`` namespace's
|
|
812
|
+
store when it is a separate one (else None)."""
|
|
813
|
+
from agent_env.store import routing
|
|
814
|
+
|
|
815
|
+
configured = {"document": self._configured_document_store, "object": self._configured_object_store,
|
|
816
|
+
"image": self._configured_image_store}[kind]()
|
|
817
|
+
local = self._separate_local_store(kind, configured) if routing.namespace_routing_enabled() else None
|
|
818
|
+
return configured, local
|
|
819
|
+
|
|
820
|
+
def _separate_local_store(self, kind: str, configured: Any) -> Any:
|
|
821
|
+
"""The ``@local`` namespace's store of ``kind``, or None when ``configured`` already is it.
|
|
822
|
+
Decided once per configured store: every store access asks."""
|
|
823
|
+
cached = self._separate_stores.get(kind)
|
|
824
|
+
if cached is not None and cached[0] is configured:
|
|
825
|
+
return cached[1]
|
|
826
|
+
local = self._compare_local_store(kind, configured)
|
|
827
|
+
self._separate_stores[kind] = (configured, local)
|
|
828
|
+
return local
|
|
829
|
+
|
|
830
|
+
def _compare_local_store(self, kind: str, configured: Any) -> Any:
|
|
831
|
+
from agent_env.store.document_store import LocalSqliteDocumentStore
|
|
832
|
+
from agent_env.store.image_store import LocalRegistryImageStore
|
|
833
|
+
from agent_env.store.image_store.oci_registry_credentials import normalize_registry_host
|
|
834
|
+
from agent_env.store.object_store import LocalFilesystemObjectStore
|
|
835
|
+
|
|
836
|
+
local = self._local_store(kind)
|
|
837
|
+
if kind == "document":
|
|
838
|
+
same = isinstance(configured, LocalSqliteDocumentStore) and configured.path.resolve() == local.path.resolve()
|
|
839
|
+
elif kind == "object":
|
|
840
|
+
same = isinstance(configured, LocalFilesystemObjectStore) and configured.root.resolve() == local.root.resolve()
|
|
841
|
+
else:
|
|
842
|
+
same = isinstance(configured, LocalRegistryImageStore) and (
|
|
843
|
+
normalize_registry_host(configured.registry_host) == normalize_registry_host(local.registry_host)
|
|
844
|
+
)
|
|
845
|
+
return None if same else local
|
|
846
|
+
|
|
847
|
+
def _routed(self, kind: str, configured: Any) -> Any:
|
|
848
|
+
"""``configured`` wrapped by namespace routing, or ``configured`` itself when it already is
|
|
849
|
+
the ``@local`` namespace's store."""
|
|
850
|
+
from agent_env.store import routing
|
|
851
|
+
|
|
852
|
+
local = self._separate_local_store(kind, configured)
|
|
853
|
+
if local is None:
|
|
854
|
+
if kind == "document":
|
|
855
|
+
raise ConfigError(
|
|
856
|
+
f"{self._local_namespace_document_store_path()} is kept for @local documents; "
|
|
857
|
+
"point [stores.document] somewhere else"
|
|
858
|
+
)
|
|
859
|
+
return configured
|
|
860
|
+
with self._routing_lock:
|
|
861
|
+
routed = self._routed_stores.get(kind)
|
|
862
|
+
if routed is None or routed.configured is not configured:
|
|
863
|
+
if kind == "document":
|
|
864
|
+
routed = routing.RoutingDocumentStore(configured, local, local.path)
|
|
865
|
+
elif kind == "object":
|
|
866
|
+
routed = routing.LocalRunObjectStore(configured, local)
|
|
867
|
+
else:
|
|
868
|
+
routed = routing.LocalRunImageStore(configured, local)
|
|
869
|
+
self._routed_stores[kind] = routed
|
|
870
|
+
return routed
|
|
871
|
+
|
|
872
|
+
def get_runner(self):
|
|
873
|
+
"""The configured Runner (cached): an explicitly-set one, else ``[runner]`` /
|
|
874
|
+
``AGENT_ENV_RUNNER`` (``local`` default -> the in-process asyncio runner)."""
|
|
875
|
+
if self._runner is None:
|
|
876
|
+
with self._runner_lock:
|
|
877
|
+
if self._runner is None:
|
|
878
|
+
self._runner = self._build_default_runner()
|
|
879
|
+
return self._runner
|
|
880
|
+
|
|
881
|
+
def set_runner(self, runner) -> None:
|
|
882
|
+
"""Install an explicit runner; get_runner() then returns it verbatim."""
|
|
883
|
+
self._runner = runner
|
|
884
|
+
|
|
885
|
+
def _build_default_runner(self):
|
|
886
|
+
from agent_env.config import loader as _loader
|
|
887
|
+
from agent_env.runner.runner import Runner
|
|
888
|
+
|
|
889
|
+
section = self._resolve_runner_section()
|
|
890
|
+
return _loader.build_store(section, Runner, secret_resolver=self._resolve_secret)
|
|
891
|
+
|
|
892
|
+
def _resolve_runner_section(self) -> dict:
|
|
893
|
+
return self.trace_section("runner").value
|
|
894
|
+
|
|
895
|
+
def _runner_alias(self, name: str) -> dict:
|
|
896
|
+
if name == _RUNNER_BACKEND_LOCAL:
|
|
897
|
+
return {"impl": "agent_env.runner.local_runner:LocalRunner", "config": {}}
|
|
898
|
+
raise ConfigError(f"Unknown AGENT_ENV_RUNNER={name!r} (expected 'local', or a [runner] table)")
|
|
899
|
+
|
|
900
|
+
def get_secret_store(self) -> SecretStore:
|
|
901
|
+
"""The secret store (cached) that ``secret:`` references resolve through: an explicitly-set
|
|
902
|
+
backend, else ``AGENT_ENV_SECRET_STORE`` / config.toml (``local`` default → env/file)."""
|
|
903
|
+
if self._secret_store is None:
|
|
904
|
+
with self._secret_store_lock:
|
|
905
|
+
if self._secret_store is None:
|
|
906
|
+
self._secret_store = self._build_default_secret_store()
|
|
907
|
+
return self._secret_store
|
|
908
|
+
|
|
909
|
+
def _resolve_secret(self, name: str) -> Optional[str]:
|
|
910
|
+
"""Bound so the secret store is built only when a ``secret:`` reference is actually met."""
|
|
911
|
+
return self.get_secret_store().get(name)
|
|
912
|
+
|
|
913
|
+
def set_secret_store(self, store: SecretStore) -> None:
|
|
914
|
+
"""Install an explicit backend; get_secret_store() then returns it verbatim."""
|
|
915
|
+
self._secret_store = store
|
|
916
|
+
|
|
917
|
+
def _build_default_secret_store(self) -> SecretStore:
|
|
918
|
+
from agent_env.store.secret_store import SecretStore
|
|
919
|
+
|
|
920
|
+
section = self._resolve_secret_section()
|
|
921
|
+
return config_loader.build_store(section, SecretStore)
|
|
922
|
+
|
|
923
|
+
def _resolve_secret_section(self) -> dict:
|
|
924
|
+
return self.trace_section("secret").value
|
|
925
|
+
|
|
926
|
+
def _secret_alias(self, name: str) -> dict:
|
|
927
|
+
if name == _SECRET_BACKEND_AWS:
|
|
928
|
+
raise ConfigError(
|
|
929
|
+
"The 'aws' secret backend has no built-in coordinates: configure a [stores.secret] "
|
|
930
|
+
"table (and unset AGENT_ENV_SECRET_STORE, which overrides it)."
|
|
931
|
+
)
|
|
932
|
+
if name == _STORE_BACKEND_LOCAL:
|
|
933
|
+
return {"impl": "agent_env.store.secret_store:LocalSecretStore", "config": {}}
|
|
934
|
+
raise ConfigError(
|
|
935
|
+
f"Unknown AGENT_ENV_SECRET_STORE={name!r} (expected 'local', or a [stores.secret] table for a hosted backend)"
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
def get_artifact_key_prefix(self) -> str:
|
|
939
|
+
"""Key prefix for every object key core builds: artifacts, image builds, env and agent
|
|
940
|
+
snapshots, changelogs, default trajectories, verifier outputs and validator fixtures (set
|
|
941
|
+
via AGENT_ENV_FIXTURE_PREFIX; empty in prod)."""
|
|
942
|
+
return f"{self.fixture_prefix}/" if self.fixture_prefix else ""
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
# One row per aliased section, carrying every coordinate that section has. The resolvers and
|
|
946
|
+
# the report both read this, so neither can drift from the other. The alias is held as a
|
|
947
|
+
# function object rather than a method name: a rename then fails at import, not at use.
|
|
948
|
+
ALIASED_SECTIONS = (
|
|
949
|
+
AliasedSection("document", "AGENT_ENV_DOCUMENT_STORE", ("stores", "document"),
|
|
950
|
+
_STORE_BACKEND_LOCAL, Config._document_alias),
|
|
951
|
+
AliasedSection("object", "AGENT_ENV_OBJECT_STORE", ("stores", "object"),
|
|
952
|
+
_STORE_BACKEND_LOCAL, Config._object_alias),
|
|
953
|
+
AliasedSection("image", "AGENT_ENV_IMAGE_STORE", ("stores", "image"),
|
|
954
|
+
_STORE_BACKEND_LOCAL, Config._image_alias),
|
|
955
|
+
AliasedSection("secret", "AGENT_ENV_SECRET_STORE", ("stores", "secret"),
|
|
956
|
+
_STORE_BACKEND_LOCAL, Config._secret_alias),
|
|
957
|
+
AliasedSection("runner", "AGENT_ENV_RUNNER", ("runner",),
|
|
958
|
+
_RUNNER_BACKEND_LOCAL, Config._runner_alias),
|
|
959
|
+
)
|
|
960
|
+
_BY_NAME = {section.name: section for section in ALIASED_SECTIONS}
|
|
961
|
+
|
|
962
|
+
_config: Optional[Config] = None
|
|
963
|
+
_config_lock = threading.Lock()
|
|
964
|
+
# Registry names this thread is building, on any Config; see Config._registry.
|
|
965
|
+
_registries_building = threading.local()
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def in_nested_registry_build() -> bool:
|
|
969
|
+
"""Whether this thread is inside a re-entered, unmemoized registry build."""
|
|
970
|
+
return _registries_building.__dict__.get("nested", 0) > 0
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def configure(
|
|
974
|
+
*,
|
|
975
|
+
default_human_a2a_url: Optional[str] = None,
|
|
976
|
+
default_a2a_agent_id: Optional[str] = None,
|
|
977
|
+
document_store: Optional[DocumentStore] = None,
|
|
978
|
+
image_store: Optional[ImageStore] = None,
|
|
979
|
+
object_store: Optional[ObjectStore] = None,
|
|
980
|
+
secret_store: Optional[SecretStore] = None,
|
|
981
|
+
) -> None:
|
|
982
|
+
"""Rebuild the process-wide config from the config file ``AGENT_ENV_CONFIG`` resolves to.
|
|
983
|
+
|
|
984
|
+
Pass ``document_store`` / ``image_store`` / ``object_store`` / ``secret_store`` to install explicit backends on the new config.
|
|
985
|
+
|
|
986
|
+
``default_human_a2a_url`` overrides config.toml ``[conversations]`` but not
|
|
987
|
+
``AGENT_ENV_HUMAN_A2A_URL``. ``default_a2a_agent_id`` overrides config.toml ``[agents]``.
|
|
988
|
+
|
|
989
|
+
Select the stage *before* calling this, by pointing ``AGENT_ENV_CONFIG`` at a per-stage
|
|
990
|
+
config file (a plugin can do this for you, for instance from a CLI root option it registers).
|
|
991
|
+
"""
|
|
992
|
+
global _config
|
|
993
|
+
overrides: dict[str, Any] = {}
|
|
994
|
+
if default_human_a2a_url is not None:
|
|
995
|
+
overrides["default_human_a2a_url"] = default_human_a2a_url
|
|
996
|
+
if default_a2a_agent_id is not None:
|
|
997
|
+
overrides["default_a2a_agent_id"] = default_a2a_agent_id
|
|
998
|
+
config_path = config_loader.discover_config_path()
|
|
999
|
+
_config = Config(**overrides)
|
|
1000
|
+
if document_store is not None:
|
|
1001
|
+
_config.set_document_store(document_store)
|
|
1002
|
+
if image_store is not None:
|
|
1003
|
+
_config.set_image_store(image_store)
|
|
1004
|
+
if object_store is not None:
|
|
1005
|
+
_config.set_object_store(object_store)
|
|
1006
|
+
if secret_store is not None:
|
|
1007
|
+
_config.set_secret_store(secret_store)
|
|
1008
|
+
logger.info("Store config resolved: %s", config_path or "(no config file)")
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def set_document_store(store: DocumentStore) -> None:
|
|
1012
|
+
"""Override the process-wide control-plane document store backend."""
|
|
1013
|
+
get_config().set_document_store(store)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def set_image_store(store: ImageStore) -> None:
|
|
1017
|
+
"""Override the process-wide image (registry) store backend."""
|
|
1018
|
+
get_config().set_image_store(store)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def set_object_store(store: ObjectStore) -> None:
|
|
1022
|
+
"""Override the process-wide object (blob) store backend."""
|
|
1023
|
+
get_config().set_object_store(store)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def set_secret_store(store: SecretStore) -> None:
|
|
1027
|
+
"""Override the process-wide secret store backend."""
|
|
1028
|
+
get_config().set_secret_store(store)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def set_runner(runner) -> None:
|
|
1032
|
+
"""Install an explicit Runner on the singleton config."""
|
|
1033
|
+
get_config().set_runner(runner)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def get_runner():
|
|
1037
|
+
"""The configured Runner (see ``[runner]`` in .agentenv/config.toml)."""
|
|
1038
|
+
return get_config().get_runner()
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def get_config() -> Config:
|
|
1042
|
+
"""The process-wide Config, built on first use.
|
|
1043
|
+
|
|
1044
|
+
Locked: the Config owns a document now, so a losing racer's — and anything built from
|
|
1045
|
+
it — would be discarded while its caller kept using it.
|
|
1046
|
+
"""
|
|
1047
|
+
global _config
|
|
1048
|
+
cached = _config
|
|
1049
|
+
if cached is not None:
|
|
1050
|
+
return cached
|
|
1051
|
+
with _config_lock:
|
|
1052
|
+
if _config is None:
|
|
1053
|
+
_config = Config()
|
|
1054
|
+
return _config
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def reset_config() -> None:
|
|
1058
|
+
"""Drop the process-wide config, so the next read resolves the file again.
|
|
1059
|
+
|
|
1060
|
+
The registries and stores the Config holds go with it, and a store singleton resolves
|
|
1061
|
+
its backend per call, so it follows. Only an explicitly installed object persists.
|
|
1062
|
+
"""
|
|
1063
|
+
global _config
|
|
1064
|
+
_config = None
|