Files
tracer-cloud--opensre/tools/registry_discovery.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

241 lines
8.3 KiB
Python

"""Tool discovery — walk tool packages and extract RegisteredTool objects.
The canonical ``tools/`` tree and each per-vendor ``integrations/<vendor>/tools``
package are walked here; the registry facade (:mod:`tools.registry`) caches the
result. Kept separate so both the facade and the static descriptor index
(:mod:`tools.registry_index`) share ``INTEGRATION_TOOL_PACKAGES`` without a
circular import.
"""
from __future__ import annotations
import importlib
import inspect
import logging
import pkgutil
from dataclasses import replace
from types import ModuleType
from core.tool_framework.base import BaseTool
from core.tool_framework.registered_tool import REGISTERED_TOOL_ATTR, RegisteredTool
logger = logging.getLogger(__name__)
# Per-vendor tool packages — when a vendor consolidates its tool code under
# ``integrations/<vendor>/tools/``, list the dotted package path here so the
# registry walks it alongside the canonical ``tools/`` package. New vendors get
# one entry each as they migrate.
INTEGRATION_TOOL_PACKAGES: tuple[str, ...] = (
"integrations.alertmanager.tools",
"integrations.argocd.tools",
"integrations.aws.tools",
"integrations.aws_lambda.tools",
"integrations.azure.tools",
"integrations.azure_sql.tools",
"integrations.betterstack.tools",
"integrations.bitbucket.tools",
"integrations.clickhouse.tools",
"integrations.cloudtrail.tools",
"integrations.cloudwatch.tools",
"integrations.coralogix.tools",
"integrations.dagster.tools",
"integrations.datadog.tools",
"integrations.ec2.tools",
"integrations.eks.tools",
"integrations.elasticsearch.tools",
"integrations.elb.tools",
"integrations.github.tools",
"integrations.gitlab.tools",
"integrations.google_docs.tools",
"integrations.grafana.tools",
"integrations.groundcover.tools",
"integrations.helm.tools",
"integrations.hermes.tools",
"integrations.honeycomb.tools",
"integrations.incident_io.tools",
"integrations.jenkins.tools",
"integrations.jira.tools",
"integrations.kafka.tools",
"integrations.mariadb.tools",
"integrations.mongodb.tools",
"integrations.mongodb_atlas.tools",
"integrations.mysql.tools",
"integrations.openclaw.tools",
"integrations.openobserve.tools",
"integrations.opensearch.tools",
"integrations.opsgenie.tools",
"integrations.pagerduty.tools",
"integrations.posthog_mcp.tools",
"integrations.postgresql.tools",
"integrations.prefect.tools",
"integrations.rabbitmq.tools",
"integrations.rds.tools",
"integrations.redis.tools",
"integrations.s3.tools",
"integrations.sentry.tools",
"integrations.sentry_mcp.tools",
"integrations.signoz.tools",
"integrations.snowflake.tools",
"integrations.splunk.tools",
"integrations.supabase.tools",
"integrations.telegram.tools",
"integrations.tempo.tools",
"integrations.temporal.tools",
"integrations.tracer.tools",
"integrations.twilio.tools",
"integrations.vercel.tools",
"integrations.victoria_logs.tools",
"integrations.x_mcp.tools",
)
_SKIP_MODULE_NAMES = {
"__pycache__",
"investigation_registry",
"registry",
}
_TOOL_MODULES_ATTR = "TOOL_MODULES"
def _iter_tool_module_names(package: ModuleType) -> list[str]:
module_names: list[str] = []
for module_info in pkgutil.iter_modules(package.__path__):
if module_info.name in _SKIP_MODULE_NAMES:
continue
if module_info.name.startswith("_") or module_info.name.endswith("_test"):
continue
module_names.append(module_info.name)
return sorted(module_names)
def _import_tool_module(package: ModuleType, module_name: str) -> ModuleType:
return importlib.import_module(_qualify_tool_module_name(package, module_name))
def _qualify_tool_module_name(package: ModuleType, module_name: str) -> str:
if module_name == package.__name__ or module_name.startswith(f"{package.__name__}."):
return module_name
return f"{package.__name__}.{module_name}"
def _iter_manifest_tool_module_names(module: ModuleType) -> tuple[str, ...]:
manifest = getattr(module, _TOOL_MODULES_ATTR, ())
if manifest is None:
return ()
if isinstance(manifest, str):
logger.warning(
"[tools] Ignoring %s.%s because it must be an iterable of module names, not a string",
module.__name__,
_TOOL_MODULES_ATTR,
)
return ()
try:
module_names = tuple(manifest)
except TypeError:
logger.warning(
"[tools] Ignoring %s.%s because it is not iterable",
module.__name__,
_TOOL_MODULES_ATTR,
)
return ()
valid_module_names: list[str] = []
for module_name in module_names:
if not isinstance(module_name, str) or not module_name:
logger.warning(
"[tools] Ignoring invalid %s entry on %s: %r",
_TOOL_MODULES_ATTR,
module.__name__,
module_name,
)
continue
valid_module_names.append(module_name)
return tuple(valid_module_names)
def _import_tool_module_or_none(package: ModuleType, module_name: str) -> ModuleType | None:
full_module_name = _qualify_tool_module_name(package, module_name)
try:
return _import_tool_module(package, module_name)
except ModuleNotFoundError as exc:
logger.warning("[tools] Skipping %s: %s", full_module_name, exc)
return None
except Exception as exc:
logger.warning(
"[tools] Skipping %s due to import failure: %s",
full_module_name,
exc,
exc_info=True,
)
return None
def iter_discovered_tool_modules(package: ModuleType) -> list[ModuleType]:
modules: list[ModuleType] = []
for module_name in _iter_tool_module_names(package):
module = _import_tool_module_or_none(package, module_name)
if module is None:
continue
modules.append(module)
for manifest_module_name in _iter_manifest_tool_module_names(module):
manifest_module = _import_tool_module_or_none(module, manifest_module_name)
if manifest_module is not None:
modules.append(manifest_module)
return modules
def _candidate_belongs_to_module(candidate: object, module_name: str) -> bool:
if isinstance(candidate, RegisteredTool):
return (candidate.origin_module or getattr(candidate.run, "__module__", "")) == module_name
if isinstance(candidate, BaseTool):
return candidate.__class__.__module__ == module_name
return getattr(candidate, "__module__", None) == module_name
def _registered_tool_from_candidate(candidate: object) -> RegisteredTool | None:
if isinstance(candidate, RegisteredTool):
if not candidate.origin_module or not candidate.origin_name:
return replace(
candidate,
origin_module=candidate.origin_module or getattr(candidate.run, "__module__", ""),
origin_name=candidate.origin_name or getattr(candidate.run, "__name__", ""),
)
return candidate
registered = getattr(candidate, REGISTERED_TOOL_ATTR, None)
if isinstance(registered, RegisteredTool):
return registered
if isinstance(candidate, BaseTool):
return RegisteredTool.from_base_tool(candidate)
return None
def collect_registered_tools_from_module(module: ModuleType) -> list[RegisteredTool]:
tools_by_name: dict[str, RegisteredTool] = {}
seen_candidate_ids: set[int] = set()
for _, candidate in inspect.getmembers(module):
if not _candidate_belongs_to_module(candidate, module.__name__):
continue
candidate_id = id(candidate)
if candidate_id in seen_candidate_ids:
continue
seen_candidate_ids.add(candidate_id)
registered = _registered_tool_from_candidate(candidate)
if registered is None:
continue
if registered.name in tools_by_name:
logger.warning(
"[tools] Duplicate tool name '%s' in module %s; keeping first definition",
registered.name,
module.__name__,
)
continue
tools_by_name[registered.name] = registered
return sorted(tools_by_name.values(), key=lambda tool: tool.name)