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,609 @@
|
|
|
1
|
+
"""Namespace-addressed stores: an ``@local/…`` id lives in the ``@local`` namespace's local stores,
|
|
2
|
+
every other id in the configured ones.
|
|
3
|
+
|
|
4
|
+
Routing runs only once a process turns it on (the CLI does, for the length of a command). Everywhere
|
|
5
|
+
else each getter returns the configured store exactly as before, so a service keeps rejecting every
|
|
6
|
+
``@`` id. The ``@local`` namespace keeps its documents in a file of its own beside the default local
|
|
7
|
+
store's, so each file only ever holds one namespace's documents; objects and images share the
|
|
8
|
+
per-user object store and local registry, whose urls and repositories say whose they are.
|
|
9
|
+
|
|
10
|
+
Entity documents route by their id. What a task run records (instances, journals, state, runs,
|
|
11
|
+
reviews, conversations, snapshots) and the objects it writes under keys of its own follow the run
|
|
12
|
+
scope ``Task.run()`` sets from its task id. A read that neither rule places reads both stores, the
|
|
13
|
+
reader's own namespace first: an ``@local`` run sees its records before the registry's, and anyone
|
|
14
|
+
else sees the registry's first. While an ``@local`` task runs, a write that would reach a configured
|
|
15
|
+
store is refused (the write fence), and a bare entity may never name an ``@local`` id (references
|
|
16
|
+
point outward only).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import sqlite3
|
|
24
|
+
import threading
|
|
25
|
+
from collections.abc import Iterator, Mapping
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from contextvars import ContextVar
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, BinaryIO, Callable, Optional, TypeVar
|
|
31
|
+
|
|
32
|
+
from agent_env.config.errors import ConfigError
|
|
33
|
+
from agent_env.store.document_store.document_store import (
|
|
34
|
+
DocumentStore,
|
|
35
|
+
Eq,
|
|
36
|
+
Filter,
|
|
37
|
+
In,
|
|
38
|
+
Sort,
|
|
39
|
+
UpdateSpec,
|
|
40
|
+
_dig,
|
|
41
|
+
is_entity_collection,
|
|
42
|
+
)
|
|
43
|
+
from agent_env.store.document_store.sqlite_document_store import LocalSqliteDocumentStore
|
|
44
|
+
from agent_env.store.ids import LOCAL_NAMESPACE, LOCAL_PREFIX, is_local_id, parse_namespace, validate_local_id
|
|
45
|
+
from agent_env.store.image_store import ImageStore, OciRegistryImageStore
|
|
46
|
+
from agent_env.store.image_store.oci_registry_credentials import (
|
|
47
|
+
RegistryAuth,
|
|
48
|
+
normalize_registry_host,
|
|
49
|
+
registry_host_from_ref,
|
|
50
|
+
)
|
|
51
|
+
from agent_env.store.object_store import ObjectStore
|
|
52
|
+
from agent_env.store.object_store.object_store import (
|
|
53
|
+
DEFAULT_CONTENT_TYPE,
|
|
54
|
+
HttpGetGrant,
|
|
55
|
+
HttpPutGrant,
|
|
56
|
+
ObjectMetadata,
|
|
57
|
+
UploadPolicy,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# The fields a run-time document names its owner by: one holding an @local id places it locally.
|
|
61
|
+
_OWNER_FIELDS = ("id", "instance_id", "task_id", "task_instance_id", "env_id", "agent_id")
|
|
62
|
+
_LOCAL_REPOSITORY_PREFIX = f"{LOCAL_NAMESPACE}/"
|
|
63
|
+
_TIMESTAMP = re.compile(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}")
|
|
64
|
+
_T = TypeVar("_T")
|
|
65
|
+
|
|
66
|
+
_enabled = False
|
|
67
|
+
_scopes = 0
|
|
68
|
+
_scopes_lock = threading.Lock()
|
|
69
|
+
_run_namespace: ContextVar[Optional[str]] = ContextVar("agent_env_run_namespace", default=None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LocalRunWriteError(ValueError):
|
|
73
|
+
"""A write an ``@local`` task run may not make, since it would land in a configured store."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OutwardReferenceError(ValueError):
|
|
77
|
+
"""A write that would point outside the ``@local`` namespace's stores at an ``@local`` id."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def enable_namespace_routing() -> None:
|
|
81
|
+
"""Route ``@local`` ids to the per-user local stores in this process. The CLI turns this on; a
|
|
82
|
+
service never does, so its stores stay exactly the configured ones."""
|
|
83
|
+
global _enabled
|
|
84
|
+
_enabled = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def disable_namespace_routing() -> None:
|
|
88
|
+
global _enabled
|
|
89
|
+
_enabled = False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def namespace_routing_enabled() -> bool:
|
|
93
|
+
return _enabled or _scopes > 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@contextmanager
|
|
97
|
+
def namespace_routing() -> Iterator[None]:
|
|
98
|
+
"""Route namespaces for the length of the block. Overlapping blocks (two commands run in one
|
|
99
|
+
process) keep routing on until the last of them ends."""
|
|
100
|
+
global _scopes
|
|
101
|
+
with _scopes_lock:
|
|
102
|
+
_scopes += 1
|
|
103
|
+
try:
|
|
104
|
+
yield
|
|
105
|
+
finally:
|
|
106
|
+
with _scopes_lock:
|
|
107
|
+
_scopes -= 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@contextmanager
|
|
111
|
+
def run_scope(owner_id: str) -> Iterator[None]:
|
|
112
|
+
"""The run in progress, a task's or a deployment the CLI makes: what it records follows its owner id's
|
|
113
|
+
namespace. A run started inside an ``@local`` run stays local, so a nested registry task can't reopen
|
|
114
|
+
the configured stores."""
|
|
115
|
+
outer = _run_namespace.get()
|
|
116
|
+
token = _run_namespace.set(LOCAL_NAMESPACE if outer == LOCAL_NAMESPACE else parse_namespace(owner_id) or "")
|
|
117
|
+
try:
|
|
118
|
+
yield
|
|
119
|
+
finally:
|
|
120
|
+
_run_namespace.reset(token)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def in_local_run() -> bool:
|
|
124
|
+
return namespace_routing_enabled() and _run_namespace.get() == LOCAL_NAMESPACE
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def refuse_in_local_run(what: str) -> None:
|
|
128
|
+
if in_local_run():
|
|
129
|
+
raise LocalRunWriteError(
|
|
130
|
+
f"an @local task run writes only to the local stores, and {what} would reach a configured store"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def refuse_local_derivation(entity_id: str, kind: str, doing: str) -> None:
|
|
135
|
+
"""Refuse ``doing`` to an ``@local`` entity whose tasks, artifacts or objects are named after it
|
|
136
|
+
in a form that doesn't keep the ``@local`` namespace yet."""
|
|
137
|
+
if is_local_id(entity_id):
|
|
138
|
+
raise ValueError(f"{entity_id!r} is an @local {kind}, and {doing} one isn't supported yet")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def refuse_local_references(entity_id: object, value: Any) -> None:
|
|
142
|
+
"""Refuse a bare entity naming an ``@local`` id anywhere in ``value``: a shared store must
|
|
143
|
+
never point into one person's local stores."""
|
|
144
|
+
path = _local_reference(value, "")
|
|
145
|
+
if path is not None:
|
|
146
|
+
raise OutwardReferenceError(
|
|
147
|
+
f"{entity_id!r} is not an @local id, so it can't reference one: {path or 'its value'} names an @local id"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _local_reference(value: Any, path: str) -> Optional[str]:
|
|
152
|
+
if isinstance(value, str):
|
|
153
|
+
return path if is_local_id(value) else None
|
|
154
|
+
if isinstance(value, Mapping):
|
|
155
|
+
items = ((f"{path}.{key}" if path else str(key), item) for key, item in value.items())
|
|
156
|
+
elif isinstance(value, (list, tuple, set)):
|
|
157
|
+
items = ((f"{path}[{index}]", item) for index, item in enumerate(value))
|
|
158
|
+
else:
|
|
159
|
+
return None
|
|
160
|
+
return next((found for item_path, item in items if (found := _local_reference(item, item_path)) is not None), None)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _update_values(update: UpdateSpec) -> dict:
|
|
164
|
+
return {"set": update.set, "add_to_set": update.add_to_set, "push": update.push}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _merge_sort(docs: list[dict], sort: Optional[Sort], *, absent_last: bool = False) -> list[dict]:
|
|
168
|
+
"""Order documents from both stores the way each store orders its own: an absent or null value
|
|
169
|
+
first ascending and last descending (``query``, ``find_one``), or last either way with
|
|
170
|
+
``absent_last`` (``latest_per_id``); a datetime (Mongo's) and an ISO timestamp string (SQLite's)
|
|
171
|
+
compare as times."""
|
|
172
|
+
if not sort or not sort.keys:
|
|
173
|
+
return docs
|
|
174
|
+
ordered = list(docs)
|
|
175
|
+
for key in reversed(sort.keys):
|
|
176
|
+
values = {id(doc): _dig(doc, key.field) for doc in ordered}
|
|
177
|
+
present = [doc for doc in ordered if values[id(doc)][0] and values[id(doc)][1] is not None]
|
|
178
|
+
absent = [doc for doc in ordered if not (values[id(doc)][0] and values[id(doc)][1] is not None)]
|
|
179
|
+
present.sort(key=lambda doc: _comparable(values[id(doc)][1]), reverse=key.descending)
|
|
180
|
+
ordered = present + absent if absent_last or key.descending else absent + present
|
|
181
|
+
return ordered
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _comparable(value: Any) -> tuple:
|
|
185
|
+
if isinstance(value, str) and _TIMESTAMP.match(value):
|
|
186
|
+
try:
|
|
187
|
+
value = datetime.fromisoformat(value)
|
|
188
|
+
except ValueError:
|
|
189
|
+
return ("str", value)
|
|
190
|
+
if isinstance(value, datetime):
|
|
191
|
+
return ("time", value if value.tzinfo else value.replace(tzinfo=timezone.utc))
|
|
192
|
+
if isinstance(value, bool):
|
|
193
|
+
return ("bool", value)
|
|
194
|
+
if isinstance(value, (int, float)):
|
|
195
|
+
return ("number", value)
|
|
196
|
+
if isinstance(value, (dict, list)):
|
|
197
|
+
return ("json", json.dumps(value, sort_keys=True, default=str))
|
|
198
|
+
return (type(value).__name__, value)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _ids_split_by_namespace(collection: str, id_field: str) -> bool:
|
|
202
|
+
"""Whether no id can be in both stores: an entity's id decides which one it is written to."""
|
|
203
|
+
return is_entity_collection(collection) and id_field == "id"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _newer(version: Any, than: Any) -> bool:
|
|
207
|
+
"""Whether ``version`` is a number above ``than``; anything but a number is older than any number."""
|
|
208
|
+
def number(value: Any) -> bool:
|
|
209
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
210
|
+
|
|
211
|
+
return number(version) and (not number(than) or version > than)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _window(docs: list[dict], limit: Optional[int], offset: Optional[int]) -> list[dict]:
|
|
215
|
+
docs = docs[offset or 0:]
|
|
216
|
+
return docs[:limit] if limit else docs
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _filter_owner(filter: Filter) -> dict:
|
|
220
|
+
"""The field values an all-equality filter pins, the document an upsert would create."""
|
|
221
|
+
return {path: preds[0].value for path, preds in filter.conditions.items() if len(preds) == 1 and isinstance(preds[0], Eq)}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _filter_is_local(filter: Filter) -> Optional[bool]:
|
|
225
|
+
"""Whether the ids a filter pins are all ``@local`` (True) or all bare (False); None when it pins
|
|
226
|
+
none, or both kinds."""
|
|
227
|
+
values: list[Any] = []
|
|
228
|
+
for pred in filter.conditions.get("id", ()):
|
|
229
|
+
if isinstance(pred, Eq):
|
|
230
|
+
values.append(pred.value)
|
|
231
|
+
elif isinstance(pred, In):
|
|
232
|
+
values.extend(pred.values)
|
|
233
|
+
kinds = {is_local_id(value) for value in values if isinstance(value, str)}
|
|
234
|
+
return kinds.pop() if len(kinds) == 1 else None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class LocalNamespaceDocumentStore(LocalSqliteDocumentStore):
|
|
238
|
+
"""The ``@local`` namespace's documents: its entities, and what its runs record."""
|
|
239
|
+
|
|
240
|
+
def check_id(self, entity_id: str) -> None:
|
|
241
|
+
if not is_local_id(entity_id):
|
|
242
|
+
raise ValueError(f"id {entity_id!r} isn't an @local id, and the @local namespace's store holds only those")
|
|
243
|
+
validate_local_id(entity_id)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class RoutingDocumentStore(DocumentStore):
|
|
247
|
+
"""The configured document store, with ``@local`` documents and ``@local`` runs' records kept in
|
|
248
|
+
the ``@local`` namespace's store. Reads never create that store; writes open it with its indexes."""
|
|
249
|
+
|
|
250
|
+
def __init__(self, configured: DocumentStore, local: DocumentStore, local_path: Path) -> None:
|
|
251
|
+
self.configured = configured
|
|
252
|
+
self.local = local
|
|
253
|
+
self._local_path = local_path
|
|
254
|
+
self._lock = threading.Lock()
|
|
255
|
+
self._pending_local_indexes: Optional[list[tuple]] = []
|
|
256
|
+
|
|
257
|
+
def find_one(self, collection: str, filter: Filter, sort: Optional[Sort] = None) -> Optional[dict]:
|
|
258
|
+
found = [
|
|
259
|
+
doc for store in self._readers(collection, filter)
|
|
260
|
+
if (doc := self._read(store, lambda: store.find_one(collection, filter, sort))) is not None
|
|
261
|
+
]
|
|
262
|
+
return _merge_sort(found, sort)[0] if found else None
|
|
263
|
+
|
|
264
|
+
def query(
|
|
265
|
+
self,
|
|
266
|
+
collection: str,
|
|
267
|
+
filter: Filter,
|
|
268
|
+
sort: Optional[Sort] = None,
|
|
269
|
+
limit: Optional[int] = None,
|
|
270
|
+
offset: Optional[int] = None,
|
|
271
|
+
) -> list[dict]:
|
|
272
|
+
readers = self._readers(collection, filter)
|
|
273
|
+
if len(readers) == 1:
|
|
274
|
+
return self._read(readers[0], lambda: readers[0].query(collection, filter, sort, limit, offset))
|
|
275
|
+
window = (offset or 0) + limit if limit else None
|
|
276
|
+
merged = [doc for store in readers for doc in self._read(store, lambda: store.query(collection, filter, sort, window))]
|
|
277
|
+
return _window(_merge_sort(merged, sort), limit, offset)
|
|
278
|
+
|
|
279
|
+
def count(self, collection: str, filter: Filter) -> int:
|
|
280
|
+
return sum(self._read(store, lambda: store.count(collection, filter)) for store in self._readers(collection, filter))
|
|
281
|
+
|
|
282
|
+
def latest_per_id(
|
|
283
|
+
self,
|
|
284
|
+
collection: str,
|
|
285
|
+
filter: Filter,
|
|
286
|
+
*,
|
|
287
|
+
id_field: str = "id",
|
|
288
|
+
version_field: str = "version",
|
|
289
|
+
sort: Optional[Sort] = None,
|
|
290
|
+
limit: Optional[int] = None,
|
|
291
|
+
offset: Optional[int] = None,
|
|
292
|
+
) -> list[dict]:
|
|
293
|
+
def latest(store: DocumentStore, **page: Any) -> list[dict]:
|
|
294
|
+
return store.latest_per_id(collection, filter, id_field=id_field, version_field=version_field, sort=sort, **page)
|
|
295
|
+
|
|
296
|
+
readers = self._readers(collection, filter)
|
|
297
|
+
if len(readers) == 1:
|
|
298
|
+
return self._read(readers[0], lambda: latest(readers[0], limit=limit, offset=offset))
|
|
299
|
+
if _ids_split_by_namespace(collection, id_field):
|
|
300
|
+
window = (offset or 0) + limit if limit else None
|
|
301
|
+
merged = [doc for store in readers for doc in self._read(store, lambda: latest(store, limit=window))]
|
|
302
|
+
else:
|
|
303
|
+
merged = self._latest_across(readers, collection, filter, id_field, version_field)
|
|
304
|
+
return _window(_merge_sort(merged, sort, absent_last=True), limit, offset)
|
|
305
|
+
|
|
306
|
+
def count_distinct(self, collection: str, filter: Filter, *, id_field: str = "id") -> int:
|
|
307
|
+
readers = self._readers(collection, filter)
|
|
308
|
+
if len(readers) == 1 or _ids_split_by_namespace(collection, id_field):
|
|
309
|
+
return sum(
|
|
310
|
+
self._read(store, lambda: store.count_distinct(collection, filter, id_field=id_field)) for store in readers
|
|
311
|
+
)
|
|
312
|
+
return len(self._latest_across(readers, collection, filter, id_field, "version"))
|
|
313
|
+
|
|
314
|
+
def _latest_across(
|
|
315
|
+
self, readers: list[DocumentStore], collection: str, filter: Filter, id_field: str, version_field: str,
|
|
316
|
+
) -> list[dict]:
|
|
317
|
+
"""Each id's latest document across both stores, where one id can be in both: the higher
|
|
318
|
+
version wins, and a tie goes to the reader's own namespace."""
|
|
319
|
+
latest: dict[str, dict] = {}
|
|
320
|
+
for store in readers:
|
|
321
|
+
docs = self._read(
|
|
322
|
+
store, lambda: store.latest_per_id(collection, filter, id_field=id_field, version_field=version_field)
|
|
323
|
+
)
|
|
324
|
+
for doc in docs:
|
|
325
|
+
key = json.dumps(_dig(doc, id_field)[1], sort_keys=True, default=str)
|
|
326
|
+
held = latest.get(key)
|
|
327
|
+
if held is None or _newer(_dig(doc, version_field)[1], _dig(held, version_field)[1]):
|
|
328
|
+
latest[key] = doc
|
|
329
|
+
return list(latest.values())
|
|
330
|
+
|
|
331
|
+
def insert(self, collection: str, doc: dict) -> None:
|
|
332
|
+
store = self._writer(collection, doc, doc)
|
|
333
|
+
self._read(store, lambda: store.insert(collection, doc))
|
|
334
|
+
|
|
335
|
+
def update(self, collection: str, filter: Filter, update: UpdateSpec, upsert: bool = False) -> int:
|
|
336
|
+
if upsert:
|
|
337
|
+
store = self._writer(collection, _filter_owner(filter), _update_values(update))
|
|
338
|
+
return self._read(store, lambda: store.update(collection, filter, update, upsert=True))
|
|
339
|
+
for store in self._updaters(collection, filter, _update_values(update)):
|
|
340
|
+
if matched := self._read(store, lambda: store.update(collection, filter, update)):
|
|
341
|
+
return matched
|
|
342
|
+
return 0
|
|
343
|
+
|
|
344
|
+
def update_one_and_get(
|
|
345
|
+
self,
|
|
346
|
+
collection: str,
|
|
347
|
+
filter: Filter,
|
|
348
|
+
update: UpdateSpec,
|
|
349
|
+
return_after: bool = True,
|
|
350
|
+
upsert: bool = False,
|
|
351
|
+
) -> Optional[dict]:
|
|
352
|
+
if upsert:
|
|
353
|
+
store = self._writer(collection, _filter_owner(filter), _update_values(update))
|
|
354
|
+
return self._read(store, lambda: store.update_one_and_get(collection, filter, update, return_after, upsert=True))
|
|
355
|
+
for store in self._updaters(collection, filter, _update_values(update)):
|
|
356
|
+
if (doc := self._read(store, lambda: store.update_one_and_get(collection, filter, update, return_after))) is not None:
|
|
357
|
+
return doc
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
def replace(self, collection: str, filter: Filter, doc: dict, upsert: bool = False) -> int:
|
|
361
|
+
if upsert:
|
|
362
|
+
store = self._writer(collection, {**_filter_owner(filter), **doc}, doc)
|
|
363
|
+
return self._read(store, lambda: store.replace(collection, filter, doc, upsert=True))
|
|
364
|
+
for store in self._updaters(collection, filter, doc):
|
|
365
|
+
if matched := self._read(store, lambda: store.replace(collection, filter, doc)):
|
|
366
|
+
return matched
|
|
367
|
+
return 0
|
|
368
|
+
|
|
369
|
+
def delete(self, collection: str, filter: Filter) -> int:
|
|
370
|
+
for store in self._updaters(collection, filter, None):
|
|
371
|
+
if deleted := self._read(store, lambda: store.delete(collection, filter)):
|
|
372
|
+
return deleted
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
def ensure_index(
|
|
376
|
+
self,
|
|
377
|
+
collection: str,
|
|
378
|
+
fields: list[str],
|
|
379
|
+
unique: bool = False,
|
|
380
|
+
ttl_seconds: Optional[int] = None,
|
|
381
|
+
) -> None:
|
|
382
|
+
self.configured.ensure_index(collection, fields, unique, ttl_seconds)
|
|
383
|
+
with self._lock:
|
|
384
|
+
if self._pending_local_indexes is not None:
|
|
385
|
+
self._pending_local_indexes.append((collection, fields, unique, ttl_seconds))
|
|
386
|
+
return
|
|
387
|
+
self.local.ensure_index(collection, fields, unique, ttl_seconds)
|
|
388
|
+
|
|
389
|
+
def check_id(self, entity_id: str) -> None:
|
|
390
|
+
if is_local_id(entity_id):
|
|
391
|
+
self.local.check_id(entity_id)
|
|
392
|
+
return
|
|
393
|
+
if LOCAL_PREFIX in entity_id:
|
|
394
|
+
raise ValueError(
|
|
395
|
+
f"id {entity_id!r} contains an @local id but isn't one: an id derived from an @local id "
|
|
396
|
+
"has to keep the @local namespace"
|
|
397
|
+
)
|
|
398
|
+
self.configured.check_id(entity_id)
|
|
399
|
+
|
|
400
|
+
def _placed(self, collection: str, filter: Filter) -> Optional[DocumentStore]:
|
|
401
|
+
if not is_entity_collection(collection):
|
|
402
|
+
return None
|
|
403
|
+
local = _filter_is_local(filter)
|
|
404
|
+
return None if local is None else self.local if local else self.configured
|
|
405
|
+
|
|
406
|
+
def _readers(self, collection: str, filter: Filter) -> list[DocumentStore]:
|
|
407
|
+
"""The stores a read consults, the reader's own namespace first: a key can be in both, when
|
|
408
|
+
an ``@local`` run and a registry run each recorded it."""
|
|
409
|
+
placed = self._placed(collection, filter)
|
|
410
|
+
if placed is self.configured:
|
|
411
|
+
return [self.configured]
|
|
412
|
+
local = [self.local] if self._local_path.exists() else []
|
|
413
|
+
if placed is self.local:
|
|
414
|
+
return local
|
|
415
|
+
return [*local, self.configured] if in_local_run() else [self.configured, *local]
|
|
416
|
+
|
|
417
|
+
def _read(self, store: DocumentStore, read: Callable[[], _T]) -> _T:
|
|
418
|
+
if store is not self.local:
|
|
419
|
+
return read()
|
|
420
|
+
try:
|
|
421
|
+
return read()
|
|
422
|
+
except (sqlite3.DatabaseError, OSError) as e:
|
|
423
|
+
raise ConfigError(
|
|
424
|
+
f"the @local store at {self._local_path} can't be read ({e}); move it aside to start a new one"
|
|
425
|
+
) from e
|
|
426
|
+
|
|
427
|
+
def _writer(self, collection: str, owner: Mapping, payload: Any) -> DocumentStore:
|
|
428
|
+
if is_entity_collection(collection):
|
|
429
|
+
if isinstance(owner.get("id"), str):
|
|
430
|
+
self.check_id(owner["id"])
|
|
431
|
+
local = isinstance(owner.get("id"), str) and is_local_id(owner["id"])
|
|
432
|
+
else:
|
|
433
|
+
local = in_local_run() or any(isinstance(owner.get(f), str) and is_local_id(owner[f]) for f in _OWNER_FIELDS)
|
|
434
|
+
if local:
|
|
435
|
+
return self._open_local()
|
|
436
|
+
self._check_configured_write(collection, owner.get("id"), payload)
|
|
437
|
+
return self.configured
|
|
438
|
+
|
|
439
|
+
def _updaters(self, collection: str, filter: Filter, payload: Any) -> Iterator[DocumentStore]:
|
|
440
|
+
"""The stores an update of ``filter``'s match tries, in order: the one its id places it in,
|
|
441
|
+
else the caller's own namespace's first. Inside an ``@local`` run only the local store is
|
|
442
|
+
written, and a match that lives only in the configured store is refused rather than missed."""
|
|
443
|
+
placed = self._placed(collection, filter)
|
|
444
|
+
local = placed is not self.configured and self._local_path.exists()
|
|
445
|
+
if placed is None and not in_local_run():
|
|
446
|
+
self._check_configured_write(collection, _filter_owner(filter).get("id"), payload)
|
|
447
|
+
yield self.configured
|
|
448
|
+
if local:
|
|
449
|
+
yield self._open_local()
|
|
450
|
+
return
|
|
451
|
+
if local:
|
|
452
|
+
yield self._open_local()
|
|
453
|
+
if placed is self.configured:
|
|
454
|
+
self._check_configured_write(collection, _filter_owner(filter).get("id"), payload)
|
|
455
|
+
yield self.configured
|
|
456
|
+
elif placed is None and self.configured.find_one(collection, filter) is not None:
|
|
457
|
+
refuse_in_local_run(f"updating {collection!r} (its record is in the configured store)")
|
|
458
|
+
|
|
459
|
+
def _check_configured_write(self, collection: str, entity_id: object, payload: Any) -> None:
|
|
460
|
+
refuse_in_local_run(f"this write to {collection!r}")
|
|
461
|
+
if is_entity_collection(collection) and payload is not None:
|
|
462
|
+
refuse_local_references(entity_id, payload)
|
|
463
|
+
|
|
464
|
+
def _open_local(self) -> DocumentStore:
|
|
465
|
+
with self._lock:
|
|
466
|
+
for spec in self._pending_local_indexes or ():
|
|
467
|
+
self._read(self.local, lambda: self.local.ensure_index(*spec))
|
|
468
|
+
self._pending_local_indexes = None
|
|
469
|
+
return self.local
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def object_store_holding(object_url: str, configured: ObjectStore, local: ObjectStore) -> ObjectStore:
|
|
473
|
+
"""The store an object_url addresses: the local store's own urls are local unless the configured
|
|
474
|
+
store owns them too, and any other url is the configured store's, as it was before routing."""
|
|
475
|
+
return local if local.owns(object_url) and not configured.owns(object_url) else configured
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def configured_store(store: Any) -> Any:
|
|
479
|
+
"""The configured store behind ``store``: a routing store's or view's configured side, else
|
|
480
|
+
``store`` itself. For reports that name the backend a user configured."""
|
|
481
|
+
return store.configured if isinstance(store, (RoutingDocumentStore, LocalRunObjectStore, LocalRunImageStore)) else store
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def image_store_holding(ref: str, configured: ImageStore, local: ImageStore) -> ImageStore:
|
|
485
|
+
host = registry_host_from_ref(ref)
|
|
486
|
+
if isinstance(local, OciRegistryImageStore) and host == normalize_registry_host(local.registry_host):
|
|
487
|
+
return local
|
|
488
|
+
return configured
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class LocalRunObjectStore(ObjectStore):
|
|
492
|
+
"""The object store an ``@local`` task run sees: keys go to the local store, an object_url to the
|
|
493
|
+
store it addresses, and a write that would reach the configured store is refused."""
|
|
494
|
+
|
|
495
|
+
def __init__(self, configured: ObjectStore, local: ObjectStore) -> None:
|
|
496
|
+
self.configured = configured
|
|
497
|
+
self.local = local
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def supports_transfer_grants(self) -> bool:
|
|
501
|
+
return self.local.supports_transfer_grants
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def max_single_upload_bytes(self) -> int | None:
|
|
505
|
+
return self.local.max_single_upload_bytes
|
|
506
|
+
|
|
507
|
+
def put(self, key: str, data: bytes, content_type: str = DEFAULT_CONTENT_TYPE, allow_overwrite: bool = False) -> str:
|
|
508
|
+
return self.local.put(key, data, content_type, allow_overwrite)
|
|
509
|
+
|
|
510
|
+
def put_file(self, key: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
|
|
511
|
+
return self.local.put_file(key, file_path, content_type)
|
|
512
|
+
|
|
513
|
+
def put_file_at(self, object_url: str, file_path: str, content_type: str = DEFAULT_CONTENT_TYPE) -> str:
|
|
514
|
+
return self._writing(object_url).put_file_at(object_url, file_path, content_type)
|
|
515
|
+
|
|
516
|
+
def get(self, object_url: str) -> bytes:
|
|
517
|
+
return self._at(object_url).get(object_url)
|
|
518
|
+
|
|
519
|
+
def exists(self, key: str) -> bool:
|
|
520
|
+
return self.local.exists(key)
|
|
521
|
+
|
|
522
|
+
def get_object_metadata(self, key: str) -> ObjectMetadata | None:
|
|
523
|
+
return self.local.get_object_metadata(key)
|
|
524
|
+
|
|
525
|
+
def get_object_metadata_at(self, object_url: str) -> ObjectMetadata | None:
|
|
526
|
+
return self._at(object_url).get_object_metadata_at(object_url)
|
|
527
|
+
|
|
528
|
+
def list(self, prefix: str) -> list[str]:
|
|
529
|
+
return self.local.list(prefix)
|
|
530
|
+
|
|
531
|
+
def list_at(self, url_prefix: str) -> list[str]:
|
|
532
|
+
return self._at(url_prefix).list_at(url_prefix)
|
|
533
|
+
|
|
534
|
+
def object_url(self, key: str) -> str:
|
|
535
|
+
return self.local.object_url(key)
|
|
536
|
+
|
|
537
|
+
def get_object_key(self, object_url: str) -> str:
|
|
538
|
+
return self._at(object_url).get_object_key(object_url)
|
|
539
|
+
|
|
540
|
+
def owns(self, object_url: str) -> bool:
|
|
541
|
+
return self.local.owns(object_url) or self.configured.owns(object_url)
|
|
542
|
+
|
|
543
|
+
def download_to_file(self, object_url: str, dest_path: str) -> None:
|
|
544
|
+
self._at(object_url).download_to_file(object_url, dest_path)
|
|
545
|
+
|
|
546
|
+
def open(self, object_url: str) -> BinaryIO:
|
|
547
|
+
return self._at(object_url).open(object_url)
|
|
548
|
+
|
|
549
|
+
def read(self, key: str) -> bytes:
|
|
550
|
+
return self.local.read(key)
|
|
551
|
+
|
|
552
|
+
def signed_get_url(self, object_url: str, expires_in: int = 3600) -> str | None:
|
|
553
|
+
return self._at(object_url).signed_get_url(object_url, expires_in)
|
|
554
|
+
|
|
555
|
+
def signed_put_url(self, object_url: str, expires_in: int = 3600) -> str | None:
|
|
556
|
+
return self._writing(object_url).signed_put_url(object_url, expires_in)
|
|
557
|
+
|
|
558
|
+
def signed_post(self, url_prefix: str, *, expires_in: int = 3600, max_bytes: int | None = None) -> dict | None:
|
|
559
|
+
return self._writing(url_prefix).signed_post(url_prefix, expires_in=expires_in, max_bytes=max_bytes)
|
|
560
|
+
|
|
561
|
+
def shared_credentials_env(self) -> dict[str, str]:
|
|
562
|
+
return self.configured.shared_credentials_env()
|
|
563
|
+
|
|
564
|
+
def issue_read_grant(self, object_url: str, *, expires_in: int = 3600) -> HttpGetGrant:
|
|
565
|
+
return self._at(object_url).issue_read_grant(object_url, expires_in=expires_in)
|
|
566
|
+
|
|
567
|
+
def issue_write_grant(self, object_url: str, *, media_type: str, max_bytes: int, expires_in: int = 3600) -> HttpPutGrant:
|
|
568
|
+
return self._writing(object_url).issue_write_grant(
|
|
569
|
+
object_url, media_type=media_type, max_bytes=max_bytes, expires_in=expires_in
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def issue_upload_policy(self, prefix_url: str, *, max_object_bytes: int, expires_in: int) -> UploadPolicy:
|
|
573
|
+
return self._writing(prefix_url).issue_upload_policy(prefix_url, max_object_bytes=max_object_bytes, expires_in=expires_in)
|
|
574
|
+
|
|
575
|
+
def _at(self, object_url: str) -> ObjectStore:
|
|
576
|
+
return object_store_holding(object_url, self.configured, self.local)
|
|
577
|
+
|
|
578
|
+
def _writing(self, object_url: str) -> ObjectStore:
|
|
579
|
+
store = self._at(object_url)
|
|
580
|
+
if store is self.configured:
|
|
581
|
+
refuse_in_local_run(f"a write to {object_url!r}")
|
|
582
|
+
return store
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class LocalRunImageStore(ImageStore):
|
|
586
|
+
"""The image store an ``@local`` task run sees: ``@local`` repositories are the local registry's,
|
|
587
|
+
pull auth comes from the store a ref names, and creating a configured repository is refused."""
|
|
588
|
+
|
|
589
|
+
def __init__(self, configured: ImageStore, local: ImageStore) -> None:
|
|
590
|
+
self.configured = configured
|
|
591
|
+
self.local = local
|
|
592
|
+
|
|
593
|
+
def image_ref(self, repository: str, tag: str) -> str:
|
|
594
|
+
return self._for(repository).image_ref(repository, tag)
|
|
595
|
+
|
|
596
|
+
def ensure_repository(self, repository: str) -> None:
|
|
597
|
+
store = self._for(repository)
|
|
598
|
+
if store is self.configured:
|
|
599
|
+
refuse_in_local_run(f"creating repository {repository!r}")
|
|
600
|
+
store.ensure_repository(repository)
|
|
601
|
+
|
|
602
|
+
def owns(self, ref: str) -> bool:
|
|
603
|
+
return image_store_holding(ref, self.configured, self.local).owns(ref)
|
|
604
|
+
|
|
605
|
+
def auth(self, ref: str) -> RegistryAuth | None:
|
|
606
|
+
return image_store_holding(ref, self.configured, self.local).auth(ref)
|
|
607
|
+
|
|
608
|
+
def _for(self, repository: str) -> ImageStore:
|
|
609
|
+
return self.local if repository.startswith(_LOCAL_REPOSITORY_PREFIX) else self.configured
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Backend-agnostic secret store abstraction + its implementations.
|
|
2
|
+
|
|
3
|
+
``secret_store`` defines the abstraction (SecretStore); ``aws_secrets_manager_secret_store``
|
|
4
|
+
and ``local_secret_store`` are implementations. ``gcp_secret_manager_secret_store`` is one
|
|
5
|
+
too, not re-exported here because it needs the ``gcp`` extra. Future backends (Vault, Azure
|
|
6
|
+
Key Vault, K8s secrets, ...) live alongside as new modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from agent_env.store.secret_store.aws_secrets_manager_secret_store import (
|
|
10
|
+
AwsSecretsManagerSecretStore,
|
|
11
|
+
)
|
|
12
|
+
from agent_env.store.secret_store.local_secret_store import LocalSecretStore
|
|
13
|
+
from agent_env.store.secret_store.secret_store import SecretStore
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"SecretStore",
|
|
17
|
+
"AwsSecretsManagerSecretStore",
|
|
18
|
+
"LocalSecretStore",
|
|
19
|
+
]
|