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,113 @@
|
|
|
1
|
+
"""Artifact Registry credentials for OciRegistryImageStore (the ``gcp`` extra)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from datetime import UTC, datetime, timedelta
|
|
10
|
+
|
|
11
|
+
import google.auth
|
|
12
|
+
from google.auth import impersonated_credentials
|
|
13
|
+
from google.api_core.exceptions import RetryError
|
|
14
|
+
from google.auth.exceptions import DefaultCredentialsError, RefreshError, TransportError
|
|
15
|
+
|
|
16
|
+
from agent_env.config.errors import ConfigError
|
|
17
|
+
from agent_env.store import _google
|
|
18
|
+
from agent_env.store.image_store.oci_registry_credentials import (
|
|
19
|
+
OciRegistryCredentials,
|
|
20
|
+
RegistryAuth,
|
|
21
|
+
normalize_registry_host,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# A token must outlast what it is handed to: an image build logs in with it to push at its end.
|
|
25
|
+
_MIN_REMAINING = timedelta(minutes=45)
|
|
26
|
+
_USERNAME = "oauth2accesstoken"
|
|
27
|
+
_TRANSIENT_IAM_CODES = frozenset({429, 500, 502, 503, 504})
|
|
28
|
+
# A mint that found IAM unreachable spent a whole retry window; callers that come just after it
|
|
29
|
+
# share its failure rather than each spending another.
|
|
30
|
+
_OUTAGE_REUSE_SECONDS = 10.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _transient(error: Exception) -> bool:
|
|
34
|
+
"""A dropped connection, or IAM declining for now. google-auth reports an IAM refusal as a
|
|
35
|
+
RefreshError carrying the response body: a 429 or 5xx, or a body that is no JSON at all, as a
|
|
36
|
+
front end's error page is. A token endpoint's refusal carries a parsed mapping, and is final."""
|
|
37
|
+
if _google.dropped_connection(error):
|
|
38
|
+
return True
|
|
39
|
+
if not isinstance(error, RefreshError) or len(error.args) < 2 or not isinstance(error.args[1], str):
|
|
40
|
+
return False
|
|
41
|
+
try:
|
|
42
|
+
body = json.loads(error.args[1])
|
|
43
|
+
except ValueError:
|
|
44
|
+
return True
|
|
45
|
+
status = body.get("error") if isinstance(body, dict) else None
|
|
46
|
+
return isinstance(status, dict) and status.get("code") in _TRANSIENT_IAM_CODES
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_MINT_RETRY = _google.RETRY.with_predicate(_transient)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GoogleAccessTokenCredentials(OciRegistryCredentials):
|
|
53
|
+
"""Docker-login material for Artifact Registry: an access token of ``service_account``, which
|
|
54
|
+
Application Default Credentials impersonate. The token is handed to sandboxes, so the account
|
|
55
|
+
should hold nothing but write access to the registry's repository."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, service_account: str) -> None:
|
|
58
|
+
if not service_account.strip():
|
|
59
|
+
raise ConfigError("GoogleAccessTokenCredentials needs a service_account")
|
|
60
|
+
self._service_account = service_account.strip()
|
|
61
|
+
self._credentials: impersonated_credentials.Credentials | None = None
|
|
62
|
+
self._request = None
|
|
63
|
+
self._lock = threading.Lock()
|
|
64
|
+
self._failure: tuple[float, Exception] | None = None
|
|
65
|
+
|
|
66
|
+
def mint(self, host: str) -> RegistryAuth:
|
|
67
|
+
arrived = time.monotonic()
|
|
68
|
+
with self._lock:
|
|
69
|
+
# A caller that queued behind a failed attempt, or arrives just after IAM proved
|
|
70
|
+
# unreachable, gets its failure rather than another retry window.
|
|
71
|
+
if self._failure is not None:
|
|
72
|
+
failed_at, failure = self._failure
|
|
73
|
+
outage = isinstance(failure, TransportError) and time.monotonic() - failed_at < _OUTAGE_REUSE_SECONDS
|
|
74
|
+
if failed_at > arrived or outage:
|
|
75
|
+
raise copy.copy(failure).with_traceback(None) from failure
|
|
76
|
+
try:
|
|
77
|
+
token = self._token()
|
|
78
|
+
except Exception as e:
|
|
79
|
+
self._failure = (time.monotonic(), e)
|
|
80
|
+
raise
|
|
81
|
+
self._failure = None
|
|
82
|
+
return RegistryAuth(registry=normalize_registry_host(host), username=_USERNAME, password=token)
|
|
83
|
+
|
|
84
|
+
def _token(self) -> str:
|
|
85
|
+
if self._credentials is None:
|
|
86
|
+
self._credentials, self._request = self._impersonate()
|
|
87
|
+
if _remaining(self._credentials) < _MIN_REMAINING:
|
|
88
|
+
try:
|
|
89
|
+
_MINT_RETRY(self._credentials.refresh)(self._request)
|
|
90
|
+
except RefreshError as e:
|
|
91
|
+
# Look the credentials up again next time, in case they were replaced.
|
|
92
|
+
self._credentials = self._request = None
|
|
93
|
+
raise ConfigError(f"Cannot mint an access token as {self._service_account}: {e}") from e
|
|
94
|
+
except RetryError as e:
|
|
95
|
+
raise TransportError(f"IAM minted no access token as {self._service_account} within the retry window: {e.cause}") from e
|
|
96
|
+
return self._credentials.token
|
|
97
|
+
|
|
98
|
+
def _impersonate(self):
|
|
99
|
+
try:
|
|
100
|
+
source, _ = google.auth.default(scopes=_google.SCOPES)
|
|
101
|
+
except (DefaultCredentialsError, OSError) as e:
|
|
102
|
+
raise ConfigError(f"No Google credentials to impersonate {self._service_account} with: {e}") from e
|
|
103
|
+
credentials = impersonated_credentials.Credentials(
|
|
104
|
+
source_credentials=source, target_principal=self._service_account, target_scopes=_google.SCOPES
|
|
105
|
+
)
|
|
106
|
+
return credentials, _google.pooled_request()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _remaining(credentials) -> timedelta:
|
|
110
|
+
if credentials.token is None:
|
|
111
|
+
return timedelta(0)
|
|
112
|
+
# google-auth keeps expiry as naive UTC.
|
|
113
|
+
return credentials.expiry - datetime.now(UTC).replace(tzinfo=None)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Backend-agnostic container image (registry) store abstraction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any, Self
|
|
8
|
+
|
|
9
|
+
from agent_env.store.image_store.oci_registry_credentials import (
|
|
10
|
+
OciRegistryCredentials,
|
|
11
|
+
RegistryAuth,
|
|
12
|
+
normalize_registry_host,
|
|
13
|
+
registry_host_from_ref,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ImageStore(ABC):
|
|
18
|
+
"""Image store interface. Images are addressed by a logical ``repository`` and ``tag``.
|
|
19
|
+
|
|
20
|
+
``image_ref`` builds a fully-qualified, pullable ref; ``auth`` returns the
|
|
21
|
+
docker-login material a remote executor applies — the store never runs docker.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_config(cls, *args: Any, **config: Any) -> Self:
|
|
26
|
+
"""Construct from a resolved config table; backends that build a client override this, taking
|
|
27
|
+
the table's keys as named parameters, which ``(*args, **config)`` lets a type checker accept."""
|
|
28
|
+
return cls(*args, **config)
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def image_ref(self, repository: str, tag: str) -> str:
|
|
32
|
+
"""Fully-qualified, pullable ref for ``(repository, tag)`` in this store."""
|
|
33
|
+
|
|
34
|
+
def ensure_repository(self, repository: str) -> None:
|
|
35
|
+
"""Create the repository if the backend requires pre-creation; no-op otherwise."""
|
|
36
|
+
|
|
37
|
+
def owns(self, ref: str) -> bool:
|
|
38
|
+
"""Whether ``ref`` is in this store, as ``auth`` decides it: a caller can tell a pull this
|
|
39
|
+
store authorizes from one it knows nothing about. The default owns nothing. Overriding
|
|
40
|
+
``owns`` does not change which refs ``auth`` logs in for."""
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def auth(self, ref: str) -> RegistryAuth | None:
|
|
45
|
+
"""Freshly-minted docker-login material for ``ref``, or None when none is needed
|
|
46
|
+
(anonymous/local registry, a public ref, or a ref not in this store)."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class OciRegistryImageStore(ImageStore):
|
|
50
|
+
"""Generic OCI registry whose repository names and credentials are configurable."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
registry_host: str,
|
|
55
|
+
repository_prefix: str = "",
|
|
56
|
+
credentials: OciRegistryCredentials | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._registry = normalize_registry_host(registry_host)
|
|
59
|
+
self._prefix = repository_prefix
|
|
60
|
+
self._credentials = credentials
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_config(
|
|
64
|
+
cls,
|
|
65
|
+
*,
|
|
66
|
+
registry_host: str,
|
|
67
|
+
repository_prefix: str = "",
|
|
68
|
+
credentials: OciRegistryCredentials | Mapping[str, object] | None = None,
|
|
69
|
+
) -> Self:
|
|
70
|
+
if isinstance(credentials, Mapping):
|
|
71
|
+
from agent_env.config import ConfigError, load_impl
|
|
72
|
+
|
|
73
|
+
credential_config = dict(credentials)
|
|
74
|
+
impl = credential_config.pop("impl", None)
|
|
75
|
+
if not impl:
|
|
76
|
+
raise ConfigError(
|
|
77
|
+
"Registry credentials are missing an 'impl' dotted path"
|
|
78
|
+
)
|
|
79
|
+
credential_cls = load_impl(impl, OciRegistryCredentials)
|
|
80
|
+
credentials = credential_cls.from_config(**credential_config)
|
|
81
|
+
elif credentials is not None and not isinstance(
|
|
82
|
+
credentials, OciRegistryCredentials
|
|
83
|
+
):
|
|
84
|
+
raise TypeError("credentials must implement OciRegistryCredentials")
|
|
85
|
+
return cls(
|
|
86
|
+
registry_host=registry_host,
|
|
87
|
+
repository_prefix=repository_prefix,
|
|
88
|
+
credentials=credentials,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def registry_host(self) -> str:
|
|
93
|
+
return self._registry
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def credentials(self) -> OciRegistryCredentials | None:
|
|
97
|
+
return self._credentials
|
|
98
|
+
|
|
99
|
+
def image_ref(self, repository: str, tag: str) -> str:
|
|
100
|
+
return f"{self._registry}/{self._repo_name(repository)}:{tag}"
|
|
101
|
+
|
|
102
|
+
def owns(self, ref: str) -> bool:
|
|
103
|
+
"""A ref on this store's registry, whatever its repository: a registry login covers the
|
|
104
|
+
whole host."""
|
|
105
|
+
return registry_host_from_ref(ref) == self._registry
|
|
106
|
+
|
|
107
|
+
def auth(self, ref: str) -> RegistryAuth | None:
|
|
108
|
+
if registry_host_from_ref(ref) != self._registry or self._credentials is None:
|
|
109
|
+
return None
|
|
110
|
+
return self._credentials.mint(self._registry)
|
|
111
|
+
|
|
112
|
+
def _repo_name(self, repository: str) -> str:
|
|
113
|
+
return f"{self._prefix}/{repository}" if self._prefix else repository
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Local OCI registry implementation of the ImageStore interface (no cloud services)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
from agent_env.store.image_store.image_store import OciRegistryImageStore
|
|
10
|
+
from agent_env.store.image_store.oci_registry_credentials import OciRegistryCredentials
|
|
11
|
+
|
|
12
|
+
_REGISTRY_CONTAINER = "agentenv-registry"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LocalRegistryImageStore(OciRegistryImageStore):
|
|
16
|
+
"""ImageStore backed by an anonymous local OCI registry.
|
|
17
|
+
|
|
18
|
+
Repositories are created automatically on push.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
registry_host: str = "localhost:5000",
|
|
24
|
+
repository_prefix: str = "",
|
|
25
|
+
credentials: OciRegistryCredentials | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(
|
|
28
|
+
registry_host=registry_host,
|
|
29
|
+
repository_prefix=repository_prefix,
|
|
30
|
+
credentials=credentials,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def ensure_repository(self, repository: str) -> None:
|
|
34
|
+
"""A local registry creates repositories on push, so this instead lazily brings up the
|
|
35
|
+
backing ``registry:2`` — idempotently, and safe to call from many concurrent pushes. A
|
|
36
|
+
no-op when something already serves the registry at ``registry_host`` (a registry you
|
|
37
|
+
run yourself, or one a concurrent push already started); it never removes a container
|
|
38
|
+
that might be serving. Called from the image-push path, so no caller manages it."""
|
|
39
|
+
port = urlsplit(f"//{self.registry_host}").port or 5000
|
|
40
|
+
if self._registry_listening(port):
|
|
41
|
+
return
|
|
42
|
+
run = self._run_registry(port)
|
|
43
|
+
if run.returncode != 0:
|
|
44
|
+
# `docker run` creates the named container atomically, so a failure is either a name
|
|
45
|
+
# conflict (our container already exists) or something unrelated — disambiguate on
|
|
46
|
+
# the existing container instead of blindly retrying, so a real error isn't masked.
|
|
47
|
+
existing_port = self._container_configured_port(_REGISTRY_CONTAINER)
|
|
48
|
+
if existing_port is None:
|
|
49
|
+
# No such container: the failure is real — the port is taken by another process,
|
|
50
|
+
# the image pull failed, or the daemon is down. Surface it now, don't wait 30s.
|
|
51
|
+
raise RuntimeError(f"could not start the local registry: {run.stderr.strip()}")
|
|
52
|
+
if existing_port != port:
|
|
53
|
+
# A stale container of ours on a different port (registry_host changed since it
|
|
54
|
+
# was created). It isn't serving our port — the check above proved that — so
|
|
55
|
+
# replacing it is safe.
|
|
56
|
+
subprocess.run(["docker", "rm", "-f", _REGISTRY_CONTAINER], capture_output=True, text=True)
|
|
57
|
+
run = self._run_registry(port)
|
|
58
|
+
if run.returncode != 0:
|
|
59
|
+
raise RuntimeError(f"could not start the local registry: {run.stderr.strip()}")
|
|
60
|
+
else:
|
|
61
|
+
# A concurrent push won the race, or a stopped container of ours is on our port:
|
|
62
|
+
# (re)start it (a no-op if it's already running) and surface a genuine failure.
|
|
63
|
+
start = subprocess.run(["docker", "start", _REGISTRY_CONTAINER], capture_output=True, text=True)
|
|
64
|
+
if start.returncode != 0:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
f"local registry container exists but could not be started: {start.stderr.strip()}"
|
|
67
|
+
)
|
|
68
|
+
deadline = time.time() + 30
|
|
69
|
+
while time.time() < deadline:
|
|
70
|
+
if self._registry_listening(port):
|
|
71
|
+
return
|
|
72
|
+
time.sleep(0.5)
|
|
73
|
+
raise RuntimeError(f"the local registry started but did not answer /v2/ on port {port} within 30s")
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _run_registry(port: int) -> subprocess.CompletedProcess:
|
|
77
|
+
return subprocess.run(
|
|
78
|
+
["docker", "run", "-d", "--restart", "unless-stopped", "--name", _REGISTRY_CONTAINER,
|
|
79
|
+
"-p", f"127.0.0.1:{port}:5000", "registry:2"],
|
|
80
|
+
capture_output=True, text=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _container_configured_port(name: str) -> int | None:
|
|
85
|
+
"""The host port ``name`` maps container-port 5000 to (works for stopped containers too),
|
|
86
|
+
or None when no such container exists — i.e. the ``docker run`` failure was not a name
|
|
87
|
+
clash but a real error (port taken elsewhere / pull / daemon)."""
|
|
88
|
+
r = subprocess.run(
|
|
89
|
+
["docker", "inspect", "-f",
|
|
90
|
+
'{{ (index (index .HostConfig.PortBindings "5000/tcp") 0).HostPort }}', name],
|
|
91
|
+
capture_output=True, text=True,
|
|
92
|
+
)
|
|
93
|
+
if r.returncode != 0:
|
|
94
|
+
return None
|
|
95
|
+
out = r.stdout.strip()
|
|
96
|
+
return int(out) if out.isdigit() else None
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _registry_listening(port: int) -> bool:
|
|
100
|
+
"""True if an OCI registry answers the ``/v2/`` ping on ``port`` (ours or one you run)."""
|
|
101
|
+
import httpx
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return httpx.get(f"http://localhost:{port}/v2/", timeout=2).status_code in (200, 401)
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Credential providers for OCI registries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from urllib.parse import urlsplit
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class RegistryAuth:
|
|
19
|
+
"""docker-login material for a registry (the password may be a short-lived token)."""
|
|
20
|
+
|
|
21
|
+
registry: str
|
|
22
|
+
username: str
|
|
23
|
+
password: str = field(repr=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OciRegistryCredentials(ABC):
|
|
27
|
+
"""Produces docker-login material for a registry.
|
|
28
|
+
|
|
29
|
+
Implementations resolve secret or identity inputs inside ``mint`` rather than
|
|
30
|
+
``__init__`` so a process-cached image store can observe credential rotation.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_config(cls, **config) -> OciRegistryCredentials:
|
|
35
|
+
return cls(**config)
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def mint(self, host: str) -> RegistryAuth | None:
|
|
39
|
+
"""Return credentials for ``host``, or ``None`` when none are available."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Docker Hub is special: unqualified image references resolve there, Docker config
|
|
43
|
+
# commonly keys its credentials by ``index.docker.io``, and pulls may use
|
|
44
|
+
# ``registry-1.docker.io``. Canonicalizing these aliases lets one credential entry
|
|
45
|
+
# cover all equivalent forms; every other registry still requires an exact host match.
|
|
46
|
+
_DOCKER_HUB_HOSTS = {
|
|
47
|
+
"docker.io",
|
|
48
|
+
"index.docker.io",
|
|
49
|
+
"registry-1.docker.io",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_registry_host(host: str) -> str:
|
|
54
|
+
"""Normalize a registry authority without weakening exact host matching."""
|
|
55
|
+
normalized = host.strip().lower()
|
|
56
|
+
if not normalized:
|
|
57
|
+
raise ValueError("registry_host must not be empty")
|
|
58
|
+
if "://" in normalized or "/" in normalized:
|
|
59
|
+
raise ValueError("registry_host must be an authority without a scheme or path")
|
|
60
|
+
return "docker.io" if normalized in _DOCKER_HUB_HOSTS else normalized
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def registry_host_from_ref(ref: str) -> str | None:
|
|
64
|
+
"""Return the normalized registry authority from a Docker/OCI image reference.
|
|
65
|
+
|
|
66
|
+
Docker treats a first path component as a registry only when it is ``localhost``
|
|
67
|
+
or contains a dot or port. Unqualified references therefore resolve to Docker Hub.
|
|
68
|
+
Schemed values are not valid image references and deliberately do not match.
|
|
69
|
+
"""
|
|
70
|
+
if not ref or "://" in ref:
|
|
71
|
+
return None
|
|
72
|
+
first, separator, _ = ref.partition("/")
|
|
73
|
+
if not first:
|
|
74
|
+
return None
|
|
75
|
+
if not separator or not (
|
|
76
|
+
first.lower() == "localhost" or "." in first or ":" in first
|
|
77
|
+
):
|
|
78
|
+
return "docker.io"
|
|
79
|
+
try:
|
|
80
|
+
return normalize_registry_host(first)
|
|
81
|
+
except ValueError:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SecretStoreCredentials(OciRegistryCredentials):
|
|
86
|
+
"""Read inline Docker-config credentials from the configured secret store."""
|
|
87
|
+
|
|
88
|
+
# The default is a secret-store lookup key, not credential material.
|
|
89
|
+
def __init__(self, secret_key: str = "registry_auths") -> None: # nosec B107
|
|
90
|
+
self._secret_key = secret_key
|
|
91
|
+
|
|
92
|
+
def mint(self, host: str) -> RegistryAuth | None:
|
|
93
|
+
from agent_env.config import ConfigError, get_config
|
|
94
|
+
|
|
95
|
+
raw = get_config().get_secret_store().get(self._secret_key)
|
|
96
|
+
if raw is None:
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
document = json.loads(raw)
|
|
100
|
+
except (TypeError, json.JSONDecodeError):
|
|
101
|
+
raise ConfigError(
|
|
102
|
+
f"Registry credential secret {self._secret_key!r} must be a JSON string "
|
|
103
|
+
"containing a Docker config 'auths' mapping; nested YAML mappings are not supported"
|
|
104
|
+
) from None
|
|
105
|
+
if not isinstance(document, Mapping):
|
|
106
|
+
raise ConfigError(
|
|
107
|
+
f"Registry credential secret {self._secret_key!r} must contain a JSON object"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if "credsStore" in document or "credHelpers" in document:
|
|
111
|
+
logger.debug(
|
|
112
|
+
"Registry credential secret %r contains Docker credential-helper settings; "
|
|
113
|
+
"agent-env only reads inline auth entries",
|
|
114
|
+
self._secret_key,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
auths = document.get("auths", document)
|
|
118
|
+
if not isinstance(auths, Mapping):
|
|
119
|
+
raise ConfigError(
|
|
120
|
+
f"Registry credential secret {self._secret_key!r} has a non-object 'auths' value"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
normalized_host = normalize_registry_host(host)
|
|
124
|
+
entry = next(
|
|
125
|
+
(
|
|
126
|
+
candidate
|
|
127
|
+
for key, candidate in auths.items()
|
|
128
|
+
if _docker_config_registry_host(str(key)) == normalized_host
|
|
129
|
+
),
|
|
130
|
+
None,
|
|
131
|
+
)
|
|
132
|
+
if entry is None:
|
|
133
|
+
return None
|
|
134
|
+
if not isinstance(entry, Mapping):
|
|
135
|
+
raise ConfigError(
|
|
136
|
+
f"Registry credential secret {self._secret_key!r} has invalid credentials "
|
|
137
|
+
f"for registry {normalized_host!r}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if entry.get("auth") is not None:
|
|
141
|
+
username, password = _decode_auth(
|
|
142
|
+
entry["auth"], self._secret_key, normalized_host
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
username = entry.get("username")
|
|
146
|
+
password = entry.get("password")
|
|
147
|
+
if not isinstance(username, str) or not isinstance(password, str):
|
|
148
|
+
raise ConfigError(
|
|
149
|
+
f"Registry credential secret {self._secret_key!r} must provide either 'auth' "
|
|
150
|
+
f"or string 'username'/'password' fields for registry {normalized_host!r}"
|
|
151
|
+
)
|
|
152
|
+
return RegistryAuth(
|
|
153
|
+
registry=normalized_host, username=username, password=password
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _docker_config_registry_host(key: str) -> str | None:
|
|
158
|
+
candidate = key.strip()
|
|
159
|
+
if not candidate:
|
|
160
|
+
return None
|
|
161
|
+
if "://" in candidate:
|
|
162
|
+
parsed = urlsplit(candidate)
|
|
163
|
+
if not parsed.hostname:
|
|
164
|
+
return None
|
|
165
|
+
try:
|
|
166
|
+
candidate = parsed.hostname
|
|
167
|
+
if parsed.port is not None:
|
|
168
|
+
candidate = f"{candidate}:{parsed.port}"
|
|
169
|
+
except ValueError:
|
|
170
|
+
return None
|
|
171
|
+
else:
|
|
172
|
+
candidate = candidate.split("/", 1)[0]
|
|
173
|
+
try:
|
|
174
|
+
return normalize_registry_host(candidate)
|
|
175
|
+
except ValueError:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _decode_auth(value: object, secret_key: str, host: str) -> tuple[str, str]:
|
|
180
|
+
if not isinstance(value, str):
|
|
181
|
+
raise _invalid_auth(secret_key, host)
|
|
182
|
+
try:
|
|
183
|
+
decoded = base64.b64decode(value, validate=True).decode()
|
|
184
|
+
username, password = decoded.split(":", 1)
|
|
185
|
+
except (binascii.Error, UnicodeDecodeError, ValueError):
|
|
186
|
+
raise _invalid_auth(secret_key, host) from None
|
|
187
|
+
return username, password
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _invalid_auth(secret_key: str, host: str):
|
|
191
|
+
from agent_env.config import ConfigError
|
|
192
|
+
|
|
193
|
+
return ConfigError(
|
|
194
|
+
f"Registry credential secret {secret_key!r} has invalid base64 'auth' "
|
|
195
|
+
f"for registry {host!r}"
|
|
196
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""On-disk state directories for the local (no-infra) stores."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from agent_env.config.errors import ConfigError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_state_dir(path: Path) -> None:
|
|
11
|
+
"""Create ``path`` if missing. A directory this creates is private to the user (``0o700``)
|
|
12
|
+
and gets a ``.gitignore`` of ``*`` so generated state is never committed; a directory that
|
|
13
|
+
already existed is left as is. A directory that cannot be created is a ``ConfigError``."""
|
|
14
|
+
try:
|
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
try:
|
|
17
|
+
path.mkdir(mode=0o700)
|
|
18
|
+
except FileExistsError:
|
|
19
|
+
if path.is_dir():
|
|
20
|
+
return
|
|
21
|
+
raise
|
|
22
|
+
(path / ".gitignore").write_text("*\n")
|
|
23
|
+
except OSError as e:
|
|
24
|
+
raise ConfigError(
|
|
25
|
+
f"cannot create the local store directory {path} ({e}); set XDG_STATE_HOME to a "
|
|
26
|
+
"writable directory, or point the store somewhere writable in config.toml"
|
|
27
|
+
) from e
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Backend-agnostic object store abstraction + its implementations.
|
|
2
|
+
|
|
3
|
+
``object_store`` defines the abstraction (ObjectStore); ``s3_object_store``,
|
|
4
|
+
``local_object_store`` and ``gcs_object_store`` are implementations. The last needs
|
|
5
|
+
the ``gcp`` extra, so it is not re-exported here: an impl pointer names its module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from agent_env.store.object_store.local_object_store import LocalFilesystemObjectStore
|
|
9
|
+
from agent_env.store.object_store.object_store import (
|
|
10
|
+
DEFAULT_CONTENT_TYPE,
|
|
11
|
+
ObjectMetadata,
|
|
12
|
+
ObjectStore,
|
|
13
|
+
UploadPolicy,
|
|
14
|
+
)
|
|
15
|
+
from agent_env.store.object_store.s3_object_store import S3ObjectStore
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ObjectStore",
|
|
19
|
+
"ObjectMetadata",
|
|
20
|
+
"UploadPolicy",
|
|
21
|
+
"DEFAULT_CONTENT_TYPE",
|
|
22
|
+
"S3ObjectStore",
|
|
23
|
+
"LocalFilesystemObjectStore",
|
|
24
|
+
]
|