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

280 lines
10 KiB
Python

"""Public integration catalog facade."""
from __future__ import annotations
import os
from typing import Any
from integrations import _catalog_impl
from integrations.store import load_integrations
def _sync_overrides() -> None:
"""Keep monkeypatch-friendly facade attributes wired into the implementation module."""
_catalog_impl.load_integrations = load_integrations
def classify_integrations(integrations: list[dict[str, Any]]) -> dict[str, Any]:
_sync_overrides()
return _catalog_impl.classify_integrations(integrations)
def load_env_integrations() -> list[dict[str, Any]]:
_sync_overrides()
return _catalog_impl.load_env_integrations()
def merge_local_integrations(
store_integrations: list[dict[str, Any]],
env_integrations: list[dict[str, Any]],
) -> list[dict[str, Any]]:
_sync_overrides()
return _catalog_impl.merge_local_integrations(store_integrations, env_integrations)
def merge_integrations_by_service(
*integration_groups: list[dict[str, Any]],
) -> list[dict[str, Any]]:
_sync_overrides()
return _catalog_impl.merge_integrations_by_service(*integration_groups)
def resolve_effective_integrations(
store_integrations: list[dict[str, Any]] | None = None,
env_integrations: list[dict[str, Any]] | None = None,
) -> dict[str, dict[str, Any]]:
_sync_overrides()
return _catalog_impl.resolve_effective_integrations(
store_integrations=store_integrations,
env_integrations=env_integrations,
)
def _env_is_set(name: str) -> bool:
return bool(os.getenv(name, "").strip())
def _any_env(*names: str) -> bool:
return any(_env_is_set(name) for name in names)
def _all_env(*names: str) -> bool:
return all(_env_is_set(name) for name in names)
def load_env_integration_services() -> list[str]:
"""Return integration services visible from plain env vars only.
This is the startup-safe companion to :func:`load_env_integrations`: it must
not resolve secrets from the OS keyring or build validated runtime configs.
It exists for surfaces that only need to display configured service names
before the first prompt.
"""
services: list[str] = []
def add(service: str, configured: bool) -> None:
if configured:
services.append(service)
add(
"grafana",
_all_env("GRAFANA_INSTANCE_URL", "GRAFANA_READ_TOKEN") or _env_is_set("GRAFANA_INSTANCES"),
)
add("datadog", _all_env("DD_API_KEY", "DD_APP_KEY") or _env_is_set("DD_INSTANCES"))
add(
"groundcover",
_any_env("GROUNDCOVER_API_KEY", "GROUNDCOVER_MCP_TOKEN", "GROUNDCOVER_INSTANCES"),
)
add("honeycomb", _any_env("HONEYCOMB_API_KEY", "HONEYCOMB_INSTANCES"))
add("coralogix", _any_env("CORALOGIX_API_KEY", "CORALOGIX_INSTANCES"))
add(
"aws",
_any_env("AWS_INSTANCES", "AWS_ROLE_ARN")
or _all_env("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"),
)
add(
"github",
(_env_is_set("GITHUB_MCP_COMMAND") and os.getenv("GITHUB_MCP_MODE", "").strip() == "stdio")
or _env_is_set("GITHUB_MCP_URL"),
)
add("sentry", _all_env("SENTRY_ORG_SLUG", "SENTRY_AUTH_TOKEN"))
add("gitlab", _env_is_set("GITLAB_ACCESS_TOKEN"))
add("mongodb", _env_is_set("MONGODB_CONNECTION_STRING"))
add(
"argocd",
_env_is_set("ARGOCD_INSTANCES")
or (
_env_is_set("ARGOCD_BASE_URL")
and (
_any_env("ARGOCD_AUTH_TOKEN", "ARGOCD_TOKEN")
or _all_env("ARGOCD_USERNAME", "ARGOCD_PASSWORD")
)
),
)
add("helm", os.getenv("OSRE_HELM_INTEGRATION", "").strip().lower() in {"1", "true", "yes"})
add("vercel", _env_is_set("VERCEL_API_TOKEN"))
add("opsgenie", _env_is_set("OPSGENIE_API_KEY"))
add("pagerduty", _env_is_set("PAGERDUTY_API_KEY"))
add("incident_io", _env_is_set("INCIDENT_IO_API_KEY"))
add("jira", _all_env("JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"))
add("discord", _env_is_set("DISCORD_BOT_TOKEN"))
add("telegram", _env_is_set("TELEGRAM_BOT_TOKEN"))
add("smtp", _env_is_set("SMTP_HOST"))
add("whatsapp", _all_env("TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_WHATSAPP_FROM"))
add(
"twilio",
_all_env("TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN")
and _any_env("TWILIO_SMS_FROM", "TWILIO_SMS_MESSAGING_SERVICE_SID"),
)
add(
"mongodb_atlas",
_all_env(
"MONGODB_ATLAS_PUBLIC_KEY", "MONGODB_ATLAS_PRIVATE_KEY", "MONGODB_ATLAS_PROJECT_ID"
),
)
add(
"openclaw",
(
_env_is_set("OPENCLAW_MCP_COMMAND")
and os.getenv("OPENCLAW_MCP_MODE", "").strip().lower() == "stdio"
)
or _env_is_set("OPENCLAW_MCP_URL"),
)
add("posthog_mcp", _any_env("POSTHOG_MCP_COMMAND", "POSTHOG_MCP_URL", "POSTHOG_MCP_AUTH_TOKEN"))
add("sentry_mcp", _any_env("SENTRY_MCP_COMMAND", "SENTRY_MCP_URL", "SENTRY_MCP_AUTH_TOKEN"))
add("x_mcp", _any_env("X_MCP_COMMAND", "X_MCP_URL", "X_MCP_AUTH_TOKEN"))
add("mariadb", _all_env("MARIADB_HOST", "MARIADB_DATABASE"))
add("opensearch", _env_is_set("OPENSEARCH_URL"))
return list(dict.fromkeys(services))
def configured_integration_services() -> list[str]:
"""Return lowercase service keys for integrations configured via env or the local store.
Single source of truth shared by the welcome banner and the REPL session so
they never disagree about which integrations are connected. Covers both
environment-variable configuration and integrations saved to ``~/.opensre``
(e.g. via ``opensre integrations setup ...`` or the first-launch GitHub
login). Never raises; returns an empty list on any failure so callers can
treat it as best-effort.
"""
services: list[str] = []
try:
env_services = load_env_integration_services()
except Exception:
env_services = []
for service in env_services:
service = str(service).strip().lower()
if service:
services.append(service)
try:
store_records = load_integrations()
except Exception:
store_records = []
for record in store_records:
if str(record.get("status", "active")).strip().lower() != "active":
continue
service = str(record.get("service", "")).strip().lower()
if service:
services.append(service)
return list(dict.fromkeys(services)) # deduplicate, preserve order
# Hosted MCP integrations that strictly require a personal API token when not
# running in ``stdio`` mode. A record carrying only a URL classifies as present
# but cannot connect, so callers must not imply it is working.
_HOSTED_MCP_TOKEN_REQUIRED: frozenset[str] = frozenset({"posthog_mcp", "sentry_mcp"})
def _hosted_mcp_missing_token(service: str, config: dict[str, Any]) -> bool:
"""Offline check for an obviously-unusable hosted MCP config.
Hosted MCP servers (non-``stdio``) authenticate with a personal API token;
a record with only a URL is "configured" but cannot connect. Mirrors the
runtime-unavailable checks in the MCP integration modules without doing any
network I/O.
"""
if service not in _HOSTED_MCP_TOKEN_REQUIRED:
return False
if str(config.get("mode", "")).strip().lower() == "stdio":
return False
return not str(config.get("auth_token", "")).strip()
def configured_integration_health() -> list[tuple[str, str]]:
"""Return ``(service, status)`` for each configured integration.
``status`` is ``"ok"`` when the stored/env config is minimally complete
enough to attempt a connection, or ``"incomplete"`` when required
credentials are missing — for example a hosted MCP record saved without an
API token, or a service whose secrets did not classify into a usable config.
The welcome banner uses this so it reflects health rather than mere presence.
Performs no network verification (startup stays fast) and never raises; on
any failure each service falls back to ``"ok"`` so the banner still lists it.
"""
services = configured_integration_services()
if not services:
return []
store_config_by_service: dict[str, dict[str, Any]] = {}
try:
store_records = load_integrations()
except Exception:
store_records = []
for record in store_records:
if str(record.get("status", "active")).strip().lower() != "active":
continue
service = str(record.get("service", "")).strip().lower()
if not service:
continue
credentials = record.get("credentials")
if isinstance(credentials, dict):
store_config_by_service.setdefault(service, credentials)
continue
instances = record.get("instances")
if isinstance(instances, list):
for instance in instances:
if not isinstance(instance, dict):
continue
instance_credentials = instance.get("credentials")
if isinstance(instance_credentials, dict):
store_config_by_service.setdefault(service, instance_credentials)
break
health: list[tuple[str, str]] = []
for service in services:
config = store_config_by_service.get(service, {})
if service == "posthog_mcp" and not config:
config = {
"mode": os.getenv("POSTHOG_MCP_MODE", "streamable-http").strip().lower(),
"auth_token": os.getenv("POSTHOG_MCP_AUTH_TOKEN", "").strip(),
}
elif service == "sentry_mcp" and not config:
config = {
"mode": os.getenv("SENTRY_MCP_MODE", "streamable-http").strip().lower(),
"auth_token": os.getenv("SENTRY_MCP_AUTH_TOKEN", "").strip(),
}
if _hosted_mcp_missing_token(service, config):
health.append((service, "incomplete"))
continue
health.append((service, "ok"))
return health
__all__ = [
"classify_integrations",
"configured_integration_health",
"configured_integration_services",
"load_env_integration_services",
"load_env_integrations",
"load_integrations",
"merge_integrations_by_service",
"merge_local_integrations",
"resolve_effective_integrations",
]