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,244 @@
|
|
|
1
|
+
"""Entry-point loaders for CLI plugins.
|
|
2
|
+
|
|
3
|
+
``agent_env.cli_plugins`` entries must resolve to top-level click commands/groups;
|
|
4
|
+
``agent_env.cli_root_options`` entries to ``click.Option`` instances that attach to the
|
|
5
|
+
root ``agent-env`` group. Broken plugins warn and are skipped; core names win over plugins;
|
|
6
|
+
two different root options on the same flag are both left off and reported as a conflict.
|
|
7
|
+
Full contract in the README ("Extending the CLI (plugins)").
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from contextlib import redirect_stdout
|
|
12
|
+
from importlib.metadata import EntryPoint, entry_points
|
|
13
|
+
from typing import Any
|
|
14
|
+
from weakref import WeakKeyDictionary
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from click.core import ParameterSource
|
|
18
|
+
|
|
19
|
+
from agent_env.plugins import _report, _requirements
|
|
20
|
+
from agent_env.plugins._discovery import discovery_error, dist_name, identity, sort_key
|
|
21
|
+
from agent_env.plugins._inventory import Claimant, Contribution, Diagnostic
|
|
22
|
+
|
|
23
|
+
CLI_PLUGINS_GROUP = "agent_env.cli_plugins"
|
|
24
|
+
CLI_ROOT_OPTIONS_GROUP = "agent_env.cli_root_options"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# What each loader did, per root group, so `agent-env plugin` can report it without reloading.
|
|
28
|
+
_OUTCOMES: WeakKeyDictionary[click.Group, dict[tuple[str, str], list[Contribution]]] = WeakKeyDictionary()
|
|
29
|
+
# Each entry-point group whose installed metadata could not be read, per root group.
|
|
30
|
+
_DISCOVERY_ERRORS: WeakKeyDictionary[click.Group, dict[str, Diagnostic]] = WeakKeyDictionary()
|
|
31
|
+
# Where each guarded root option came from, for the warning and the record when its callback fails.
|
|
32
|
+
_SOURCES: WeakKeyDictionary[click.Option, tuple[EntryPoint, str]] = WeakKeyDictionary()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cli_contributions(group: click.Group) -> dict[tuple[str, str], list[Contribution]]:
|
|
36
|
+
"""The CLI entry points loaded onto ``group``, by (distribution, version), with their outcome."""
|
|
37
|
+
return {key: list(found) for key, found in _OUTCOMES.get(group, {}).items()}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cli_discovery_errors(group: click.Group) -> dict[str, Diagnostic]:
|
|
41
|
+
"""The CLI entry-point groups that could not be read while loading onto ``group``, with why."""
|
|
42
|
+
return dict(_DISCOVERY_ERRORS.get(group, {}))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_cli_plugins(group: click.Group) -> None:
|
|
46
|
+
"""Register installed ``agent_env.cli_plugins`` entry points onto ``group``."""
|
|
47
|
+
added_by: dict[str, tuple[str, EntryPoint]] = {}
|
|
48
|
+
for ep, origin, plugin in _discover(group, CLI_PLUGINS_GROUP, "CLI plugin"):
|
|
49
|
+
if _incompatible(group, ep, CLI_PLUGINS_GROUP, origin):
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
command = _load(ep)
|
|
53
|
+
except (Exception, SystemExit) as exc:
|
|
54
|
+
_warn(f"{origin} failed to load and was skipped: {exc!r}")
|
|
55
|
+
_record(group, ep, CLI_PLUGINS_GROUP, "failed", code=_report.LOAD_FAILED,
|
|
56
|
+
reason=f"failed to load: {exc!r}")
|
|
57
|
+
continue
|
|
58
|
+
if not isinstance(command, click.Command):
|
|
59
|
+
_warn(f"{origin} is not a click.Command and was skipped")
|
|
60
|
+
_record(group, ep, CLI_PLUGINS_GROUP, "failed", code=_report.INVALID_PLUGIN,
|
|
61
|
+
reason="is not a click.Command")
|
|
62
|
+
continue
|
|
63
|
+
# The entry-point name, as in every group: what the report, --help and the user all use.
|
|
64
|
+
name = ep.name
|
|
65
|
+
if name in group.commands:
|
|
66
|
+
_warn(f"{origin} clashes with existing command {name!r} and was skipped")
|
|
67
|
+
if name in added_by:
|
|
68
|
+
owner, owner_ep = added_by[name]
|
|
69
|
+
_record(group, ep, CLI_PLUGINS_GROUP, "skipped", code=_report.NAME_CONFLICT,
|
|
70
|
+
reason=f"clashes with existing command {name!r} from plugin {owner}",
|
|
71
|
+
conflicts_with=(_claimant(owner_ep),))
|
|
72
|
+
else:
|
|
73
|
+
_record(group, ep, CLI_PLUGINS_GROUP, "skipped", code=_report.BUILTIN_NAME,
|
|
74
|
+
reason=f"clashes with existing command {name!r}")
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
group.add_command(command, name=name)
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
_warn(f"{origin} could not be registered and was skipped: {exc!r}")
|
|
80
|
+
_record(group, ep, CLI_PLUGINS_GROUP, "failed", code=_report.INVALID_PLUGIN,
|
|
81
|
+
reason=f"could not be registered: {exc!r}")
|
|
82
|
+
continue
|
|
83
|
+
added_by[name] = (plugin, ep)
|
|
84
|
+
_record(group, ep, CLI_PLUGINS_GROUP, "active")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_cli_root_options(group: click.Group) -> None:
|
|
88
|
+
"""Attach installed ``agent_env.cli_root_options`` entry points to ``group``.
|
|
89
|
+
|
|
90
|
+
Each must be an optional ``click.Option`` with ``expose_value=False``: it is parsed before
|
|
91
|
+
the subcommand and acts through its callback (which click also invokes with ``None`` when
|
|
92
|
+
the flag is absent), never through the root callback's arguments. A flag core already owns
|
|
93
|
+
is skipped with a warning. Different options on the same flag are all left off and recorded
|
|
94
|
+
as a conflict: a collision never silently picks a winner, and the CLI still starts, so
|
|
95
|
+
`plugin remove` can fix it. One option object exported twice, even by two packages, is one
|
|
96
|
+
callback, so it is attached once.
|
|
97
|
+
"""
|
|
98
|
+
core_flags = set(group.context_settings.get("help_option_names") or ["--help"])
|
|
99
|
+
core_flags.update(flag for param in group.params for flag in (*param.opts, *param.secondary_opts))
|
|
100
|
+
candidates: list[tuple[EntryPoint, str, str, click.Option]] = []
|
|
101
|
+
for ep, origin, plugin in _discover(group, CLI_ROOT_OPTIONS_GROUP, "CLI root option"):
|
|
102
|
+
if _incompatible(group, ep, CLI_ROOT_OPTIONS_GROUP, origin):
|
|
103
|
+
continue
|
|
104
|
+
try:
|
|
105
|
+
option = _load(ep)
|
|
106
|
+
except (Exception, SystemExit) as exc:
|
|
107
|
+
_warn(f"{origin} failed to load and was skipped: {exc!r}")
|
|
108
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "failed", code=_report.LOAD_FAILED,
|
|
109
|
+
reason=f"failed to load: {exc!r}")
|
|
110
|
+
continue
|
|
111
|
+
if not isinstance(option, click.Option):
|
|
112
|
+
_warn(f"{origin} is not a click.Option and was skipped")
|
|
113
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "failed", code=_report.INVALID_PLUGIN,
|
|
114
|
+
reason="is not a click.Option")
|
|
115
|
+
continue
|
|
116
|
+
if option.expose_value or option.required:
|
|
117
|
+
_warn(f"{origin} must be optional with expose_value=False and was skipped")
|
|
118
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "failed", code=_report.INVALID_PLUGIN,
|
|
119
|
+
reason="must be optional with expose_value=False")
|
|
120
|
+
continue
|
|
121
|
+
clash = sorted(set(_flags(option)) & core_flags)
|
|
122
|
+
if clash:
|
|
123
|
+
_warn(f"{origin} clashes with core root option {clash[0]!r} and was skipped")
|
|
124
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "skipped", code=_report.BUILTIN_NAME,
|
|
125
|
+
reason=f"clashes with core root option {clash[0]!r}")
|
|
126
|
+
continue
|
|
127
|
+
candidates.append((ep, origin, plugin, option))
|
|
128
|
+
for ep, origin, _, option in candidates:
|
|
129
|
+
# Other option objects on one of its flags; the same object listed twice is not a rival.
|
|
130
|
+
rivals = [(other_ep, other_plugin, set(_flags(other)) & set(_flags(option)))
|
|
131
|
+
for other_ep, _, other_plugin, other in candidates if other is not option]
|
|
132
|
+
rivals = [(other_ep, other_plugin, shared) for other_ep, other_plugin, shared in rivals if shared]
|
|
133
|
+
if rivals:
|
|
134
|
+
flags = ", ".join(repr(flag) for flag in sorted(set().union(*(shared for *_, shared in rivals))))
|
|
135
|
+
reason = f"root option {flags} is also registered by {' and '.join(p for _, p, _ in rivals)}"
|
|
136
|
+
_warn(f"{origin} was not attached: {reason}")
|
|
137
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "conflict", code=_report.NAME_CONFLICT, reason=reason,
|
|
138
|
+
conflicts_with=tuple(_claimant(other_ep) for other_ep, _, _ in rivals))
|
|
139
|
+
continue
|
|
140
|
+
if option in group.params:
|
|
141
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "active", code=_report.ALREADY_ATTACHED,
|
|
142
|
+
reason="the same option object is already attached")
|
|
143
|
+
continue
|
|
144
|
+
_guard(option, ep, origin)
|
|
145
|
+
group.params.append(option)
|
|
146
|
+
_record(group, ep, CLI_ROOT_OPTIONS_GROUP, "active")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _incompatible(group: click.Group, ep: EntryPoint, kind: str, origin: str) -> bool:
|
|
150
|
+
"""Record and warn about ``ep`` when its distribution's requirements exclude the installed agent-env."""
|
|
151
|
+
unmet = _requirements.incompatibility(getattr(ep, "dist", None))
|
|
152
|
+
if unmet is None:
|
|
153
|
+
return False
|
|
154
|
+
_warn(f"{origin} was skipped: it {unmet}")
|
|
155
|
+
_record(group, ep, kind, "failed", code=_report.INCOMPATIBLE_CORE, reason=unmet)
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _flags(option: click.Option) -> tuple[str, ...]:
|
|
160
|
+
return (*option.opts, *option.secondary_opts)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _claimant(ep: EntryPoint) -> Claimant:
|
|
164
|
+
return Claimant(*identity(ep), getattr(ep, "value", "") or "")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _guard(option: click.Option, ep: EntryPoint, origin: str) -> None:
|
|
168
|
+
"""Wrap ``option``'s callback so that, if it raises while the flag is absent, it is skipped and
|
|
169
|
+
recorded as failed: click calls it on every invocation, so it would otherwise stop every
|
|
170
|
+
command, `plugin remove` included. A flag the user gave still raises. What it prints with the
|
|
171
|
+
flag absent goes to stderr, so it cannot corrupt a command's `--json`. The option object stays
|
|
172
|
+
the plugin's own, so code holding it still finds it in the root group's params."""
|
|
173
|
+
_SOURCES[option] = (ep, origin)
|
|
174
|
+
callback = option.callback
|
|
175
|
+
if callback is None or getattr(callback, "_agent_env_guard", False):
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
def call(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
|
|
179
|
+
given = ctx.get_parameter_source(param.name) if param.name else None
|
|
180
|
+
absent = given in (None, ParameterSource.DEFAULT)
|
|
181
|
+
try:
|
|
182
|
+
if not absent:
|
|
183
|
+
return callback(ctx, param, value)
|
|
184
|
+
with redirect_stdout(sys.stderr):
|
|
185
|
+
return callback(ctx, param, value)
|
|
186
|
+
except (Exception, SystemExit) as exc:
|
|
187
|
+
source, where = _SOURCES.get(param, (ep, origin))
|
|
188
|
+
if not absent:
|
|
189
|
+
if isinstance(exc, (click.ClickException, click.exceptions.Exit, click.exceptions.Abort)):
|
|
190
|
+
raise
|
|
191
|
+
via = f" (set by ${param.envvar})" if given == ParameterSource.ENVIRONMENT and param.envvar else ""
|
|
192
|
+
raise click.ClickException(f"{where}{via} failed: {exc!r}") from exc
|
|
193
|
+
_warn(f"{where} failed with its flag absent and was skipped: {exc!r}")
|
|
194
|
+
_mark(ctx.find_root().command, source, f"its callback failed with the flag absent: {exc!r}")
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
call._agent_env_guard = True # type: ignore[attr-defined]
|
|
198
|
+
option.callback = call
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _mark(group: click.Group, ep: EntryPoint, reason: str) -> None:
|
|
202
|
+
"""Record an attached root option as failed after all."""
|
|
203
|
+
found = _OUTCOMES.get(group, {}).get(identity(ep), [])
|
|
204
|
+
for i, c in enumerate(found):
|
|
205
|
+
if (c.group, c.name) == (CLI_ROOT_OPTIONS_GROUP, ep.name):
|
|
206
|
+
found[i] = Contribution(c.group, c.name, c.value, "failed", code=_report.LOAD_FAILED, reason=reason)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _discover(group: click.Group, group_name: str, label: str) -> list[tuple[EntryPoint, str, str]]:
|
|
210
|
+
"""``(entry point, origin for messages, plugin label)`` per installed entry point, by name."""
|
|
211
|
+
try:
|
|
212
|
+
eps = sorted(entry_points(group=group_name), key=sort_key)
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
reason = discovery_error(exc)
|
|
215
|
+
_warn(f"{label} discovery failed; none loaded: {reason}")
|
|
216
|
+
_DISCOVERY_ERRORS.setdefault(group, {})[group_name] = Diagnostic(_report.ENTRY_POINTS_UNREADABLE, reason)
|
|
217
|
+
return []
|
|
218
|
+
found = []
|
|
219
|
+
for ep in eps:
|
|
220
|
+
dist = dist_name(ep)
|
|
221
|
+
plugin = f"{ep.name!r}" + (f" from {dist!r}" if dist else "")
|
|
222
|
+
found.append((ep, f"{label} {plugin}", plugin))
|
|
223
|
+
return found
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _load(ep: EntryPoint) -> Any:
|
|
227
|
+
# What a plugin prints while it is imported must not land in a command's stdout, e.g. --json.
|
|
228
|
+
with redirect_stdout(sys.stderr):
|
|
229
|
+
return ep.load()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _record(
|
|
233
|
+
group: click.Group, ep: EntryPoint, kind: str, status: str, *, code: str | None = None, reason: str | None = None,
|
|
234
|
+
conflicts_with: tuple[Claimant, ...] = (),
|
|
235
|
+
) -> None:
|
|
236
|
+
contribution = Contribution(
|
|
237
|
+
kind, ep.name, getattr(ep, "value", "") or "", status, code=code, reason=reason, conflicts_with=conflicts_with
|
|
238
|
+
)
|
|
239
|
+
_OUTCOMES.setdefault(group, {}).setdefault(identity(ep), []).append(contribution)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _warn(message: str) -> None:
|
|
243
|
+
# Logging isn't configured yet at import time; warn on stderr instead.
|
|
244
|
+
click.echo(f"Warning: {message}", err=True)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Reading installed entry points: which distribution claims which name in each plugin group.
|
|
2
|
+
|
|
3
|
+
Internal: the public contract is ``agent_env.plugins``. Discovery reads installed metadata only
|
|
4
|
+
and imports no plugin code; ``_registration`` imports a group's plugins when its registry is built.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from contextlib import suppress
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from importlib.metadata import EntryPoint, distributions, entry_points
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Plugin:
|
|
20
|
+
"""Where an entry-point registration came from."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
value: str
|
|
24
|
+
dist: str
|
|
25
|
+
version: str
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
source = f"{self.dist} {self.version}".strip() or "an unreadable distribution"
|
|
29
|
+
return f"{self.name!r} from {source} ({self.value})"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def claims(group: str) -> dict[str, list[tuple[Plugin, EntryPoint]]]:
|
|
33
|
+
"""Every distinct claim per entry-point name in ``group``, in a deterministic order."""
|
|
34
|
+
eps = sorted(entry_points(group=group), key=sort_key)
|
|
35
|
+
found: dict[str, list[tuple[Plugin, EntryPoint]]] = {}
|
|
36
|
+
for ep in eps:
|
|
37
|
+
plugin = Plugin(ep.name, getattr(ep, "value", "") or "", dist_name(ep), dist_version(ep))
|
|
38
|
+
seen = found.setdefault(ep.name, [])
|
|
39
|
+
# The same distribution found twice on the path is one registration, not a conflict.
|
|
40
|
+
if all((p.dist, p.value) != (plugin.dist, plugin.value) for p, _ in seen):
|
|
41
|
+
seen.append((plugin, ep))
|
|
42
|
+
return found
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def discover(group: str) -> dict[str, list[tuple[Plugin, EntryPoint]]]:
|
|
46
|
+
"""``claims(group)``, or none with a warning when installed metadata cannot be read."""
|
|
47
|
+
try:
|
|
48
|
+
return claims(group)
|
|
49
|
+
except Exception:
|
|
50
|
+
logger.warning("Entry-point discovery for %s failed; no plugins loaded", group, exc_info=True)
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def discovery_error(exc: Exception) -> str:
|
|
55
|
+
"""``exc`` from reading installed entry points, naming the distributions that cannot be read."""
|
|
56
|
+
unreadable = []
|
|
57
|
+
with suppress(Exception):
|
|
58
|
+
for dist in distributions():
|
|
59
|
+
try:
|
|
60
|
+
_ = dist.entry_points # parses entry_points.txt
|
|
61
|
+
except Exception:
|
|
62
|
+
unreadable.append(str(getattr(dist, "_path", None) or "a distribution with no path"))
|
|
63
|
+
where = f" (unreadable entry points: {', '.join(unreadable)})" if unreadable else ""
|
|
64
|
+
return f"{type(exc).__name__}: {exc}{where}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def identity(ep: EntryPoint) -> tuple[str, str]:
|
|
68
|
+
"""The (name, version) a contribution is listed under. A distribution with unreadable metadata
|
|
69
|
+
is named by where it is installed, so two such packages are not merged."""
|
|
70
|
+
name, version = dist_name(ep), dist_version(ep)
|
|
71
|
+
if name:
|
|
72
|
+
return name, version
|
|
73
|
+
try:
|
|
74
|
+
where = getattr(getattr(ep, "dist", None), "_path", None)
|
|
75
|
+
except Exception:
|
|
76
|
+
where = None
|
|
77
|
+
return (f"(unreadable metadata: {Path(where).name})" if where else "(unreadable metadata)"), version
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def sort_key(ep: EntryPoint) -> tuple[str, str, str]:
|
|
81
|
+
# ep.value tie-breaks even when both colliding dists have unreadable metadata.
|
|
82
|
+
return (ep.name, dist_name(ep), getattr(ep, "value", "") or "")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def dist_name(ep: EntryPoint) -> str:
|
|
86
|
+
# Tolerates corrupt/missing distribution metadata — the name is only a label.
|
|
87
|
+
try:
|
|
88
|
+
return getattr(getattr(ep, "dist", None), "name", "") or ""
|
|
89
|
+
except Exception:
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def dist_version(ep: EntryPoint) -> str:
|
|
94
|
+
try:
|
|
95
|
+
return getattr(getattr(ep, "dist", None), "version", "") or ""
|
|
96
|
+
except Exception:
|
|
97
|
+
return ""
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""``agent_env.plugins.inventory``: every installed plugin's contributions, and what became of each.
|
|
2
|
+
|
|
3
|
+
Internal: the public names are re-exported from ``agent_env.plugins``. The registries are built on
|
|
4
|
+
a throwaway Config that reads the caller's document, so the Config in use keeps its registries and
|
|
5
|
+
its ``load_failures()``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Mapping
|
|
11
|
+
from dataclasses import KW_ONLY, dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Any, Literal, Optional
|
|
15
|
+
|
|
16
|
+
from agent_env.config import runtime
|
|
17
|
+
from agent_env.config import snapshot as config_snapshot
|
|
18
|
+
from agent_env.config.errors import ConfigError
|
|
19
|
+
from agent_env.plugins import _discovery, _registration, _report, _requirements
|
|
20
|
+
|
|
21
|
+
Status = Literal["active", "replaced", "failed", "skipped", "conflict", "blocked", "unloaded"]
|
|
22
|
+
|
|
23
|
+
# Statuses a group-level error does not override: each already says why the name is missing.
|
|
24
|
+
_SETTLED = frozenset({"failed", "skipped", "conflict"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Diagnostic:
|
|
29
|
+
"""Why a group, the config or entry-point discovery failed."""
|
|
30
|
+
|
|
31
|
+
code: str
|
|
32
|
+
reason: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class Claimant:
|
|
37
|
+
"""Another entry point that claims the same name."""
|
|
38
|
+
|
|
39
|
+
package: str
|
|
40
|
+
version: str
|
|
41
|
+
value: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class Replacement:
|
|
46
|
+
"""The config that registers a different class under a contribution's name."""
|
|
47
|
+
|
|
48
|
+
file: Optional[Path]
|
|
49
|
+
table: str
|
|
50
|
+
impl: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Contribution:
|
|
55
|
+
"""One entry point a distribution declares, and what became of it. ``code`` says why it has
|
|
56
|
+
its status, and is set exactly when ``reason`` is."""
|
|
57
|
+
|
|
58
|
+
group: str
|
|
59
|
+
name: str
|
|
60
|
+
value: str
|
|
61
|
+
status: Status
|
|
62
|
+
_: KW_ONLY
|
|
63
|
+
code: Optional[str] = None
|
|
64
|
+
reason: Optional[str] = None
|
|
65
|
+
replaced_by: Optional[Replacement] = None
|
|
66
|
+
conflicts_with: tuple[Claimant, ...] = ()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Distribution:
|
|
71
|
+
"""An installed distribution that declares entry points in the type groups."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
version: str
|
|
75
|
+
contributions: tuple[Contribution, ...]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Inventory:
|
|
80
|
+
"""Every installed plugin distribution, and the error each group's real build would raise."""
|
|
81
|
+
|
|
82
|
+
distributions: tuple[Distribution, ...]
|
|
83
|
+
# With load=False, only what metadata proves: that no config file was found.
|
|
84
|
+
group_errors: Mapping[str, Diagnostic]
|
|
85
|
+
# Groups whose installed entry points could not be read, so none of their plugins is listed.
|
|
86
|
+
discovery_errors: Mapping[str, Diagnostic]
|
|
87
|
+
config_path: Optional[Path]
|
|
88
|
+
config_error: Optional[Diagnostic]
|
|
89
|
+
loaded: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
for _public in (Claimant, Contribution, Diagnostic, Distribution, Inventory, Replacement):
|
|
93
|
+
_public.__module__ = "agent_env.plugins"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _groups() -> dict[str, tuple[Callable[[], Any], Callable[[runtime.Config], Any]]]:
|
|
97
|
+
"""Per group: its built-in names, and how the probe builds it."""
|
|
98
|
+
# Imported here, not at the top: every registry imports agent_env.plugins, and so this module.
|
|
99
|
+
from agent_env.artifact.registry import ARTIFACT_REGISTRY
|
|
100
|
+
from agent_env.env import registry as env_registry
|
|
101
|
+
from agent_env.explorer.plugin import load_plugins
|
|
102
|
+
from agent_env.providers.env_providers import env_provider
|
|
103
|
+
from agent_env.providers.sandbox_providers.sandbox_provider import _BUILTIN_SANDBOX_PROVIDERS
|
|
104
|
+
from agent_env.providers.env_state.env_state_provider import _BUILTIN_STATE_PROVIDERS
|
|
105
|
+
from agent_env.task_step import registry as task_step_registry
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
_registration.ENVS: (env_registry._builtin_registry, lambda probe: probe.env_registry()),
|
|
109
|
+
_registration.ARTIFACTS: (lambda: ARTIFACT_REGISTRY, lambda probe: probe.artifact_registry()),
|
|
110
|
+
_registration.TASK_STEPS: (task_step_registry._builtin_registry, lambda probe: probe.task_step_registry()),
|
|
111
|
+
_registration.SANDBOX_PROVIDERS: (lambda: _BUILTIN_SANDBOX_PROVIDERS, lambda probe: probe.sandbox_registry()),
|
|
112
|
+
_registration.STATE_PROVIDERS: (lambda: _BUILTIN_STATE_PROVIDERS, lambda probe: probe.state_registry()),
|
|
113
|
+
_registration.ENV_PROVIDERS: (env_provider._builtin_env_providers, lambda probe: probe.env_provider_registry()),
|
|
114
|
+
_registration.EXPLORER_PLUGINS: (dict, lambda probe: load_plugins(source=probe)),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def inventory(config: Optional[runtime.Config] = None, *, load: bool = True) -> Inventory:
|
|
119
|
+
"""Every installed distribution declaring entry points in the type groups, with a status per
|
|
120
|
+
contribution.
|
|
121
|
+
|
|
122
|
+
``load=False`` reads installed metadata only: no plugin code runs, and a status that needs a
|
|
123
|
+
build (``active``, ``replaced``, ``failed``) is reported as ``unloaded``. ``load=True`` imports
|
|
124
|
+
the plugins, constructs the explorer plugins, and builds each group on a throwaway Config that
|
|
125
|
+
reads ``config``'s document (default: the process Config). ``config`` keeps its registries and
|
|
126
|
+
its ``load_failures()``; like any registry build, this pins its document first if nothing has
|
|
127
|
+
yet. Importing or constructing a plugin can still have process-wide effects, such as a package
|
|
128
|
+
that sets ``AGENT_ENV_CONFIG`` on import.
|
|
129
|
+
"""
|
|
130
|
+
base = config or runtime.get_config()
|
|
131
|
+
try:
|
|
132
|
+
document = base._document()
|
|
133
|
+
missing = None
|
|
134
|
+
except ConfigError as exc:
|
|
135
|
+
# No config file was found: every real build raises this before merging anything, so the
|
|
136
|
+
# plugins are checked against an empty document and then blocked.
|
|
137
|
+
document, missing = config_snapshot.Snapshot(path=None, _document={}), exc
|
|
138
|
+
if missing is not None:
|
|
139
|
+
config_error = Diagnostic(_report.CONFIG_NOT_FOUND, str(missing))
|
|
140
|
+
elif document.error is not None:
|
|
141
|
+
config_error = Diagnostic(_report.CONFIG_UNREADABLE, str(document.error))
|
|
142
|
+
else:
|
|
143
|
+
config_error = None
|
|
144
|
+
# A malformed document is used as it is, so each build raises its parse error where a real one does.
|
|
145
|
+
probe = _registration.InventoryProbe(_snapshot=document) if load else None
|
|
146
|
+
group_errors: dict[str, Diagnostic] = {}
|
|
147
|
+
discovery_errors: dict[str, Diagnostic] = {}
|
|
148
|
+
by_distribution: dict[tuple[str, str], list[Contribution]] = {}
|
|
149
|
+
groups = _groups()
|
|
150
|
+
for group, (builtins_of, build) in groups.items():
|
|
151
|
+
try:
|
|
152
|
+
claims = _discovery.claims(group)
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
discovery_errors[group] = Diagnostic(_report.ENTRY_POINTS_UNREADABLE, _discovery.discovery_error(exc))
|
|
155
|
+
continue
|
|
156
|
+
if not claims:
|
|
157
|
+
continue
|
|
158
|
+
builtins = frozenset(builtins_of())
|
|
159
|
+
registrations = None
|
|
160
|
+
if probe is not None:
|
|
161
|
+
try:
|
|
162
|
+
build(probe)
|
|
163
|
+
except (Exception, SystemExit) as exc:
|
|
164
|
+
group_errors[group] = Diagnostic(_build_error_code(exc, document), f"{type(exc).__name__}: {exc}")
|
|
165
|
+
registrations = probe.registrations.get(group)
|
|
166
|
+
if missing is not None:
|
|
167
|
+
group_errors[group] = Diagnostic(_report.CONFIG_NOT_FOUND, f"{type(missing).__name__}: {missing}")
|
|
168
|
+
for name, found in claims.items():
|
|
169
|
+
for plugin, ep in found:
|
|
170
|
+
contribution = _classify(group, name, plugin, ep, found, builtins, registrations, document.path)
|
|
171
|
+
if group in group_errors and contribution.status not in _SETTLED:
|
|
172
|
+
error = group_errors[group]
|
|
173
|
+
contribution = Contribution(
|
|
174
|
+
group, name, plugin.value, "blocked", code=error.code, reason=error.reason
|
|
175
|
+
)
|
|
176
|
+
by_distribution.setdefault(_discovery.identity(ep), []).append(contribution)
|
|
177
|
+
order = list(groups)
|
|
178
|
+
distributions = tuple(
|
|
179
|
+
Distribution(dist, version, tuple(sorted(found, key=lambda c: (order.index(c.group), c.name))))
|
|
180
|
+
for (dist, version), found in sorted(by_distribution.items(), key=lambda item: (item[0][0].lower(), item[0][1]))
|
|
181
|
+
)
|
|
182
|
+
return Inventory(
|
|
183
|
+
distributions=distributions,
|
|
184
|
+
group_errors=MappingProxyType(group_errors),
|
|
185
|
+
discovery_errors=MappingProxyType(discovery_errors),
|
|
186
|
+
config_path=document.path,
|
|
187
|
+
config_error=config_error,
|
|
188
|
+
loaded=load,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _classify(
|
|
193
|
+
group: str,
|
|
194
|
+
name: str,
|
|
195
|
+
plugin: _discovery.Plugin,
|
|
196
|
+
ep: Any,
|
|
197
|
+
found: list[tuple[_discovery.Plugin, Any]],
|
|
198
|
+
builtins: frozenset[str],
|
|
199
|
+
registrations: Optional[_registration.Registrations],
|
|
200
|
+
config_path: Optional[Path],
|
|
201
|
+
) -> Contribution:
|
|
202
|
+
# Same precedence as merge: a built-in name is skipped for every claimant, before conflicts.
|
|
203
|
+
if name in builtins:
|
|
204
|
+
return Contribution(
|
|
205
|
+
group, name, plugin.value, "skipped", code=_report.BUILTIN_NAME, reason="clashes with a built-in"
|
|
206
|
+
)
|
|
207
|
+
# Read from metadata, so it is reported without loading. A claim that cannot load claims nothing,
|
|
208
|
+
# so it takes no part in a conflict, as in a build.
|
|
209
|
+
if (unmet := _requirements.incompatibility(getattr(ep, "dist", None))) is not None:
|
|
210
|
+
return Contribution(group, name, plugin.value, "failed", code=_report.INCOMPATIBLE_CORE, reason=unmet)
|
|
211
|
+
found = _usable(found)
|
|
212
|
+
if len(found) > 1:
|
|
213
|
+
others = [(other, other_ep) for other, other_ep in found if other != plugin]
|
|
214
|
+
alone = all((other.dist, other.version) == (plugin.dist, plugin.version) for other, _ in found)
|
|
215
|
+
who = "; ".join(str(other) for other, _ in others)
|
|
216
|
+
return Contribution(
|
|
217
|
+
group, name, plugin.value, "conflict", code=_report.NAME_CONFLICT,
|
|
218
|
+
reason=(f"this package declares the name more than once: {who}; report it to its author" if alone
|
|
219
|
+
else f"also registered by {who}; remove all but one with `agent-env plugin remove <package>`"),
|
|
220
|
+
conflicts_with=tuple(_claimant(other, other_ep) for other, other_ep in others),
|
|
221
|
+
)
|
|
222
|
+
if registrations is None:
|
|
223
|
+
return Contribution(
|
|
224
|
+
group, name, plugin.value, "unloaded", code=_report.NOT_LOADED, reason="not imported (installed metadata only)"
|
|
225
|
+
)
|
|
226
|
+
if name in registrations.added:
|
|
227
|
+
return Contribution(group, name, plugin.value, "active")
|
|
228
|
+
if name in registrations.released:
|
|
229
|
+
table, impl, replacement = registrations.released[name]
|
|
230
|
+
if replacement is registrations.classes.get(name):
|
|
231
|
+
return Contribution(group, name, plugin.value, "active")
|
|
232
|
+
where = f"[{table}] of {config_path}" if config_path else f"[{table}]"
|
|
233
|
+
return Contribution(
|
|
234
|
+
group, name, plugin.value, "replaced", code=_report.REPLACED_BY_CONFIG,
|
|
235
|
+
reason=f"config names {impl!r} in {where}", replaced_by=Replacement(config_path, table, impl),
|
|
236
|
+
)
|
|
237
|
+
if name in registrations.failures:
|
|
238
|
+
reason = registrations.failures[name].removeprefix(f"registered by {plugin} but ")
|
|
239
|
+
return Contribution(group, name, plugin.value, "failed", code=registrations.codes[name], reason=reason)
|
|
240
|
+
# Unreachable while every registry records what it did; visible, not silent, if not.
|
|
241
|
+
return Contribution(
|
|
242
|
+
group, name, plugin.value, "unloaded", code=_report.STATUS_UNKNOWN, reason="its status could not be determined"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _usable(found: list[tuple[_discovery.Plugin, Any]]) -> list[tuple[_discovery.Plugin, Any]]:
|
|
247
|
+
"""The claims whose requirements admit this agent-env; the others cannot load."""
|
|
248
|
+
return [(p, ep) for p, ep in found if _requirements.incompatibility(getattr(ep, "dist", None)) is None]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _claimant(plugin: _discovery.Plugin, ep: Any) -> Claimant:
|
|
252
|
+
package, version = _discovery.identity(ep)
|
|
253
|
+
return Claimant(package, version, plugin.value)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _build_error_code(exc: BaseException, document: config_snapshot.Snapshot) -> str:
|
|
257
|
+
"""The code for what a group's build raised."""
|
|
258
|
+
if document.error is not None and exc is document.error:
|
|
259
|
+
return _report.CONFIG_UNREADABLE
|
|
260
|
+
if isinstance(exc, ConfigError):
|
|
261
|
+
return _report.CONFIG_INVALID
|
|
262
|
+
return _report.GROUP_BUILD_FAILED
|