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

1476 lines
48 KiB
Python

"""Interactive CLI for managing local integrations (~/.opensre/integrations.json).
Usage:
python -m integrations setup <service>
python -m integrations list
python -m integrations show <service>
python -m integrations remove <service>
python -m integrations verify [service] [--send-slack-test]
"""
from __future__ import annotations
import json
import sys
from typing import TYPE_CHECKING, Any, NoReturn, cast
import questionary
from platform.terminal.theme import (
ANSI_BOLD,
ANSI_DIM,
ANSI_RESET,
DEVICE_CODE_ANSI,
)
if TYPE_CHECKING:
from integrations.github.mcp import GitHubMcpDisplayDetailLevel
from integrations.gitlab import DEFAULT_GITLAB_BASE_URL
from integrations.openclaw import build_openclaw_config, validate_openclaw_config
from integrations.posthog_mcp import (
DEFAULT_POSTHOG_MCP_URL,
build_posthog_mcp_config,
validate_posthog_mcp_config,
)
from integrations.registry import SUPPORTED_SETUP_SERVICES, resolve_management_service
from integrations.sentry_mcp import (
DEFAULT_SENTRY_MCP_URL,
build_sentry_mcp_config,
validate_sentry_mcp_config,
)
from integrations.store import (
STORE_PATH,
get_integration,
list_integrations,
remove_integration,
upsert_integration,
)
from integrations.verify import (
SUPPORTED_VERIFY_SERVICES,
format_verification_results,
verification_exit_code,
verify_integrations,
)
from integrations.x_mcp import (
DEFAULT_X_MCP_URL,
build_x_mcp_config,
validate_x_mcp_config,
)
_B = ANSI_BOLD
_R = ANSI_RESET
_DIM = ANSI_DIM
def _json_echo(data: Any) -> None:
print(json.dumps(data, indent=2, default=str))
_SECRET_KEYS = frozenset(
{
"api_token",
"api_key",
"api_private_key",
"app_key",
"bearer_token",
"bot_token",
"password",
"secret_access_key",
"session_token",
"jwt_token",
"webhook_url",
"auth_token",
"connection_string",
}
)
def _p(label: str, default: str = "", secret: bool = False) -> str:
try:
if secret:
result = questionary.password(f" {label}").ask()
else:
result = questionary.text(f" {label}", default=default).ask()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(1)
if result is None:
print("\nAborted.")
sys.exit(1)
return result.strip() or default
def _die(msg: str) -> NoReturn:
print(f" error: {msg}", file=sys.stderr)
sys.exit(1)
def _prompt_github_repo_report_level() -> GitHubMcpDisplayDetailLevel:
"""Ask how much repository access detail to print after a successful validation."""
try:
sel = questionary.select(
" How much repository detail should we show?",
choices=[
questionary.Choice("Brief (recommended) — no repo names", value="summary"),
questionary.Choice("Standard — scope summary only", value="standard"),
questionary.Choice("Expanded — include repo names", value="full"),
],
default="summary",
).ask()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(1)
if sel is None:
return "summary"
if sel in ("summary", "standard", "full"):
from integrations.github.mcp import GitHubMcpDisplayDetailLevel as _Detail
return cast(_Detail, sel)
return "summary"
def _parse_port(raw: str, default: int = 3306) -> int:
"""Parse a port string, returning *default* for invalid or out-of-range values."""
try:
port = int(raw)
except (ValueError, TypeError):
return default
if port < 1 or port > 65535:
return default
return port
def _mask(obj: Any) -> Any:
if isinstance(obj, dict):
return {
k: (v[:4] + "****" if isinstance(v, str) and v else "****")
if k in _SECRET_KEYS
else _mask(v)
for k, v in obj.items()
}
if isinstance(obj, list):
return [_mask(i) for i in obj]
return obj
# ─── setup flows ──────────────────────────────────────────────────────────────
def _setup_grafana() -> None:
endpoint = _p("Instance URL (e.g. https://myorg.grafana.net)")
api_key = _p("Service account token", secret=True)
if not endpoint or not api_key:
_die("endpoint and api_key are required.")
upsert_integration("grafana", {"credentials": {"endpoint": endpoint, "api_key": api_key}})
def _setup_datadog() -> None:
api_key = _p("API key", secret=True)
app_key = _p("Application key", secret=True)
site = _p("Site", default="datadoghq.com")
if not api_key or not app_key:
_die("api_key and app_key are required.")
upsert_integration(
"datadog", {"credentials": {"api_key": api_key, "app_key": app_key, "site": site}}
)
def _setup_groundcover() -> None:
api_key = _p("Service-account API key", secret=True)
mcp_url = _p("MCP URL", default="https://mcp.groundcover.com/api/mcp")
tenant_uuid = _p("Tenant UUID (optional, for multi-workspace accounts)")
backend_id = _p("Backend ID (optional, for multi-backend tenants)")
timezone = _p("Timezone", default="UTC")
if not api_key:
_die("api_key is required.")
credentials: dict[str, str] = {
"api_key": api_key,
"mcp_url": mcp_url,
"timezone": timezone,
}
if tenant_uuid:
credentials["tenant_uuid"] = tenant_uuid
if backend_id:
credentials["backend_id"] = backend_id
upsert_integration("groundcover", {"credentials": credentials})
def _setup_honeycomb() -> None:
api_key = _p("Configuration API key", secret=True)
dataset = _p("Dataset slug or __all__", default="__all__")
base_url = _p("API URL", default="https://api.honeycomb.io")
if not api_key:
_die("api_key is required.")
upsert_integration(
"honeycomb",
{"credentials": {"api_key": api_key, "dataset": dataset, "base_url": base_url}},
)
def _setup_coralogix() -> None:
api_key = _p("DataPrime API key", secret=True)
base_url = _p("API URL", default="https://api.coralogix.com")
application_name = _p("Application name (optional)")
subsystem_name = _p("Subsystem name (optional)")
if not api_key or not base_url:
_die("api_key and base_url are required.")
upsert_integration(
"coralogix",
{
"credentials": {
"api_key": api_key,
"base_url": base_url,
"application_name": application_name,
"subsystem_name": subsystem_name,
}
},
)
def _setup_aws() -> None:
choice = questionary.select(
"AWS authentication method:",
choices=[
questionary.Choice("IAM Role ARN", value="1"),
questionary.Choice("Access Key + Secret", value="2"),
],
instruction="(use arrow keys)",
).ask()
if choice is None:
print("\nAborted.")
sys.exit(1)
region = _p("Region", default="us-east-1")
if choice == "1":
role_arn = _p("IAM Role ARN")
if not role_arn:
_die("role_arn is required.")
upsert_integration(
"aws",
{
"role_arn": role_arn,
"external_id": _p("External ID (optional)"),
"credentials": {"region": region},
},
)
else:
access_key = _p("AWS_ACCESS_KEY_ID", secret=True)
secret_key = _p("AWS_SECRET_ACCESS_KEY", secret=True)
if not access_key or not secret_key:
_die("access_key and secret_key are required.")
upsert_integration(
"aws",
{
"credentials": {
"access_key_id": access_key,
"secret_access_key": secret_key,
"session_token": _p("Session token (optional)"),
"region": region,
}
},
)
def _setup_slack() -> None:
webhook_url = _p("Slack webhook URL", secret=True)
if not webhook_url:
_die("webhook_url is required.")
upsert_integration("slack", {"credentials": {"webhook_url": webhook_url}})
def _setup_opensearch() -> None:
url = _p("URL (e.g. https://my-cluster.us-east-1.es.amazonaws.com)")
if not url:
_die("url is required.")
creds: dict[str, Any] = {"url": url}
auth_choice = questionary.select(
"OpenSearch authentication method:",
choices=[
questionary.Choice("Username + Password (HTTP Basic Auth)", value="basic"),
questionary.Choice("API key", value="api_key"),
questionary.Choice("None (security disabled)", value="none"),
],
instruction="(use arrow keys)",
).ask()
if auth_choice is None:
print("\nAborted.")
sys.exit(1)
if auth_choice == "api_key":
api_key = _p("API key", secret=True)
if not api_key:
_die("api_key is required.")
creds["api_key"] = api_key
elif auth_choice == "basic":
username = _p("Username", default="admin")
password = _p("Password", secret=True)
if not username or not password:
_die("username and password are required for basic auth.")
creds["username"] = username
creds["password"] = password
upsert_integration("opensearch", {"credentials": creds})
def _setup_rds() -> None:
host = _p("Host (e.g. mydb.xxxx.us-east-1.rds.amazonaws.com)")
port = _p("Port", default="5432")
database = _p("Database name")
username = _p("Username")
password = _p("Password", secret=True)
if not host or not database or not username:
_die("host, database, and username are required.")
upsert_integration(
"rds",
{
"credentials": {
"host": host,
"port": int(port) if port.isdigit() else 5432,
"database": database,
"username": username,
"password": password,
}
},
)
def _setup_tracer() -> None:
base_url = _p("Tracer web app URL", default="http://localhost:3000")
jwt_token = _p("JWT token", secret=True)
if not base_url or not jwt_token:
_die("base_url and jwt_token are required.")
upsert_integration("tracer", {"credentials": {"base_url": base_url, "jwt_token": jwt_token}})
def _setup_vercel() -> None:
api_token = _p("Vercel API token", secret=True)
team_id = _p("Team ID (optional for personal accounts)")
if not api_token:
_die("api_token is required.")
upsert_integration("vercel", {"credentials": {"api_token": api_token, "team_id": team_id}})
def _setup_betterstack() -> None:
query_endpoint = _p(
"Better Stack SQL query endpoint (e.g. https://eu-nbg-2-connect.betterstackdata.com)"
)
username = _p("Better Stack username (Integrations > Connect ClickHouse HTTP client)")
password = _p("Better Stack password", secret=True)
sources_raw = _p(
"Better Stack sources, comma-separated base IDs from dashboard (optional hint for the planner)"
)
if not query_endpoint or not username:
_die("query_endpoint and username are required.")
sources = [part.strip() for part in (sources_raw or "").split(",") if part.strip()]
upsert_integration(
"betterstack",
{
"credentials": {
"query_endpoint": query_endpoint,
"username": username,
"password": password,
"sources": sources,
}
},
)
def _setup_incident_io() -> None:
api_key = _p("incident.io API key", secret=True)
base_url = _p("API base URL override (optional)")
if not api_key:
_die("api_key is required.")
upsert_integration(
"incident_io",
{
"credentials": {
"api_key": api_key,
"base_url": base_url,
}
},
)
def _github_browser_authorize() -> str | None:
"""Run GitHub device-flow browser authorization.
Returns the access token, or ``None`` when the flow is unavailable so the
caller can fall back to manual token entry.
"""
from integrations.github.mcp_oauth import (
GitHubDeviceCode,
GitHubDeviceFlowError,
authorize_github_via_device_flow,
)
def _show(code: GitHubDeviceCode) -> None:
print()
print(f" 1. Your browser will open {code.verification_uri}")
print(" (if it doesn't open automatically, visit that URL yourself).")
print(
f" 2. Enter this one-time code when GitHub asks: {DEVICE_CODE_ANSI}{code.user_code}{_R}"
)
print(" 3. Approve the request for OpenSRE.")
print()
print(f" {_DIM}Waiting for you to approve in the browser… (Ctrl-C to cancel){_R}")
print()
print(" Sign in to GitHub in your browser (device authorization):")
print(f" {_DIM}Requesting a one-time code from GitHub…{_R}")
try:
token = authorize_github_via_device_flow(on_prompt=_show)
except GitHubDeviceFlowError as err:
print(f" Browser authorization unavailable: {err}", file=sys.stderr)
return None
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(1)
except Exception as err: # network/transport issues
print(f" Browser authorization failed: {err}", file=sys.stderr)
return None
print(f" {_B}Authorized.{_R} Saved a GitHub token from the browser sign-in.")
return token.access_token
def _setup_github_auth_token(mode: str) -> str:
"""Resolve a GitHub MCP auth token, offering browser sign-in for remote modes."""
if mode == "stdio":
return _p(
"GitHub PAT / auth token (optional if the server authenticates upstream)",
secret=True,
)
auth_method = questionary.select(
" How do you want to connect OpenSRE to GitHub?",
choices=[
questionary.Choice(
"Sign in with GitHub in your browser (opens a page, enter a one-time code)",
value="browser",
),
questionary.Choice("Paste a personal access token (PAT)", value="token"),
questionary.Choice("Skip — the MCP server authenticates upstream", value="none"),
],
default="browser",
).ask()
if auth_method is None:
print("\nAborted.")
sys.exit(1)
if auth_method == "none":
return ""
if auth_method == "browser":
token = _github_browser_authorize()
if token:
return token
print(" Falling back to manual token entry.")
return _p("GitHub PAT / auth token", secret=True)
def _github_advanced_setup(credentials: dict[str, Any]) -> tuple[str, str]:
"""Prompt the advanced GitHub MCP knobs and return (repo_view, repo_visibility).
Mutates ``credentials`` in place with mode/url/command/args/auth_token/toolsets.
"""
from integrations.github.mcp import (
DEFAULT_GITHUB_MCP_TOOLSETS,
DEFAULT_GITHUB_MCP_URL,
)
# Transport is fixed to Streamable HTTP. In practice it is the only mode anyone
# selects, and SSE/stdio are deprecated for the hosted GitHub MCP server. The
# transport prompt was removed on purpose — do NOT reintroduce a transport
# selection or a stdio branch here.
mode = "streamable-http"
credentials["mode"] = mode
url = _p("MCP URL", default=DEFAULT_GITHUB_MCP_URL)
if not url:
_die("url is required for remote MCP modes.")
credentials["url"] = url
credentials["auth_token"] = _setup_github_auth_token(mode)
toolsets = _p("Toolsets", default=",".join(DEFAULT_GITHUB_MCP_TOOLSETS))
credentials["toolsets"] = [part.strip() for part in toolsets.split(",") if part.strip()]
repo_view = questionary.select(
" Which repository view should we use to verify access?",
choices=[
questionary.Choice("Auto (recommended)", value="auto"),
questionary.Choice("Your repositories", value="user"),
questionary.Choice("Accessible repositories", value="accessible"),
questionary.Choice("Starred repositories", value="starred"),
questionary.Choice("Search: user:<your_login>", value="search_user"),
],
default="auto",
).ask()
if repo_view is None:
print("\nAborted.")
sys.exit(1)
repo_visibility = questionary.select(
" Filter repositories by visibility (best-effort)",
choices=[
questionary.Choice("Any (recommended)", value="any"),
questionary.Choice("Public only", value="public"),
questionary.Choice("Private only", value="private"),
],
default="any",
).ask()
if repo_visibility is None:
print("\nAborted.")
sys.exit(1)
return repo_view, repo_visibility
def _setup_github() -> str | None:
"""Configure + validate + save the GitHub MCP integration.
Returns the authenticated GitHub login on success (``None`` if the validated
result carried no login), so callers like the first-launch gate can propagate
the username. Exits the process on validation failure.
"""
from integrations.github.mcp import (
DEFAULT_GITHUB_MCP_MODE,
DEFAULT_GITHUB_MCP_TOOLSETS,
DEFAULT_GITHUB_MCP_URL,
GitHubMcpDisplayDetailLevel,
GitHubMcpRepoView,
GitHubMcpRepoVisibilityFilter,
build_github_mcp_config,
format_github_mcp_validation_cli_report,
print_github_mcp_validation_report,
validate_github_mcp_config,
)
print(" Connect OpenSRE to GitHub through the hosted GitHub MCP server.")
advanced = questionary.confirm(
" Customize advanced settings (transport, server URL, toolsets, repo scope)?",
default=False,
).ask()
if advanced is None:
print("\nAborted.")
sys.exit(1)
credentials: dict[str, Any] = {}
repo_view: str = "auto"
repo_visibility: str = "any"
if advanced:
repo_view, repo_visibility = _github_advanced_setup(credentials)
else:
credentials["mode"] = DEFAULT_GITHUB_MCP_MODE
credentials["url"] = DEFAULT_GITHUB_MCP_URL
credentials["auth_token"] = _setup_github_auth_token(DEFAULT_GITHUB_MCP_MODE)
credentials["toolsets"] = list(DEFAULT_GITHUB_MCP_TOOLSETS)
print("\n Validating GitHub MCP integration...")
mcp_config = build_github_mcp_config(credentials)
result = validate_github_mcp_config(
mcp_config,
repo_view=cast(GitHubMcpRepoView, repo_view),
repo_visibility=cast(GitHubMcpRepoVisibilityFilter, repo_visibility),
)
if result.ok:
# The simple path stays concise: identity + tool availability, no repo dump.
# Only the advanced path offers the verbose repo listing.
level = (
_prompt_github_repo_report_level()
if advanced
else cast(GitHubMcpDisplayDetailLevel, "summary")
)
print()
print_github_mcp_validation_report(result, detail_level=level)
else:
for line in format_github_mcp_validation_cli_report(result).splitlines():
print(f" {line}")
sys.exit(1)
if result.authenticated_user:
# Persist the resolved GitHub login as a non-secret credential field so
# surfaces like the welcome banner can greet the user by their GitHub
# handle instead of the local system username.
credentials["username"] = result.authenticated_user
upsert_integration("github", {"credentials": credentials})
if result.authenticated_user:
from platform.analytics.cli import identify_github_username
identify_github_username(result.authenticated_user)
return result.authenticated_user
def _setup_gitlab() -> None:
base_url = _p("Gitlab base URL", default=DEFAULT_GITLAB_BASE_URL)
auth_token = _p("Gitlab access token", secret=True)
upsert_integration(
"gitlab",
{"credentials": {"base_url": base_url, "auth_token": auth_token}},
)
def _setup_sentry() -> None:
base_url = _p("Sentry URL", default="https://sentry.io")
organization_slug = _p("Organization slug")
auth_token = _p("Auth token", secret=True)
project_slug = _p("Project slug (optional)")
if not organization_slug or not auth_token:
_die("organization_slug and auth_token are required.")
upsert_integration(
"sentry",
{
"credentials": {
"base_url": base_url,
"organization_slug": organization_slug,
"auth_token": auth_token,
"project_slug": project_slug,
}
},
)
def _setup_mongodb() -> None:
connection_string = _p(
"Connection string (e.g. mongodb+srv://user:pass@cluster.example.net)", secret=True
)
database = _p("Database name")
auth_source = _p("Auth source", default="admin")
tls_choice = questionary.select(
"TLS enabled?",
choices=[
questionary.Choice("Yes", value="1"),
questionary.Choice("No", value="0"),
],
instruction="(use arrow keys)",
).ask()
if tls_choice is None:
print("\nAborted.")
sys.exit(1)
tls = tls_choice == "1"
if not connection_string:
_die("connection_string is required.")
upsert_integration(
"mongodb",
{
"credentials": {
"connection_string": connection_string,
"database": database,
"auth_source": auth_source,
"tls": tls,
}
},
)
def _setup_redis() -> None:
host = _p("Host (e.g. localhost or redis.example.net)")
if not host:
_die("host is required.")
port_input = _p("Port", default="6379")
username = _p("Username (leave blank unless using Redis ACLs)")
password = _p("Password (leave blank if not set)", secret=True)
db_input = _p("Database number", default="0")
ssl_choice = questionary.select(
"Use TLS?",
choices=[
questionary.Choice("No", value="0"),
questionary.Choice("Yes", value="1"),
],
instruction="(use arrow keys)",
).ask()
if ssl_choice is None:
print("\nAborted.")
sys.exit(1)
ssl = ssl_choice == "1"
try:
port = int(port_input)
except (TypeError, ValueError):
_die(f"port: {port_input} is invalid")
try:
db = int(db_input)
except (TypeError, ValueError):
_die(f"db: {db_input} is invalid")
upsert_integration(
"redis",
{
"credentials": {
"host": host,
"port": port,
"username": username,
"password": password,
"db": db,
"ssl": ssl,
}
},
)
def _register_discord_slash_command(application_id: str, bot_token: str) -> None:
import httpx
url = f"https://discord.com/api/v10/applications/{application_id}/commands"
payload = {
"name": "investigate",
"description": "Trigger an OpenSRE investigation",
"options": [
{
"name": "alert",
"description": "Alert JSON or description",
"type": 3,
"required": True,
}
],
}
resp = httpx.put(url, json=[payload], headers={"Authorization": f"Bot {bot_token}"}, timeout=10)
if resp.is_success:
print(" ✓ /investigate slash command registered.")
else:
print(f" ⚠ Slash command registration failed ({resp.status_code}): {resp.text}")
def _setup_discord() -> None:
bot_token = _p("Discord bot token", secret=True)
application_id = _p("Discord application ID")
public_key = _p("Discord public key (from Developer Portal)")
default_channel_id = _p("Default channel ID (optional)")
upsert_integration(
"discord",
{
"credentials": {
"bot_token": bot_token,
"application_id": application_id,
"public_key": public_key,
"default_channel_id": default_channel_id,
}
},
)
_register_discord_slash_command(application_id, bot_token)
def _setup_telegram() -> None:
from integrations.telegram.verifier import verify_telegram
bot_token = _p("Telegram bot token", secret=True)
if not bot_token:
_die("bot_token is required.")
default_chat_id = _p("Default chat ID (optional)")
print("\n Validating Telegram bot token...")
result = verify_telegram("setup", {"bot_token": bot_token})
if result["status"] != "passed":
_die(result["detail"])
print(f" {result['detail']}")
upsert_integration(
"telegram",
{
"credentials": {
"bot_token": bot_token,
"default_chat_id": default_chat_id or None,
}
},
)
print(" Next:")
print(" - opensre integrations verify telegram")
def _setup_smtp() -> None:
host = _p("SMTP host (e.g. smtp.gmail.com)")
from_address = _p("From email address")
if not host or not from_address:
_die("host and from_address are required.")
port = _parse_port(_p("SMTP port", default="587"), default=587)
security = (_p("Security mode (starttls/ssl/none)", default="starttls") or "starttls").strip()
username = _p("Username (optional)")
password = _p("Password (optional; leave blank when username is blank)", secret=True)
if bool(username) != bool(password):
_die("username and password must both be set, or both be empty.")
upsert_integration(
"smtp",
{
"credentials": {
"host": host,
"port": port,
"security": security,
"username": username,
"password": password,
"from_address": from_address,
"default_to": _p("Default recipient email (optional)") or None,
}
},
)
def _setup_whatsapp() -> None:
account_sid = _p("Twilio Account SID (starts with AC...)")
auth_token = _p("Twilio Auth Token", secret=True)
from_number = _p("Twilio WhatsApp From number (e.g. whatsapp:+14155238886)")
default_to = _p("Default recipient phone number (optional, e.g. +1234567890)")
if not account_sid or not auth_token or not from_number:
_die("account_sid, auth_token, and from_number are required.")
upsert_integration(
"whatsapp",
{
"credentials": {
"account_sid": account_sid,
"auth_token": auth_token,
"from_number": from_number,
"default_to": default_to,
}
},
)
def _setup_twilio() -> None:
"""Wizard for the Twilio SMS integration.
WhatsApp delivery is configured separately via ``setup whatsapp``.
"""
account_sid = _p("Twilio Account SID (starts with AC...)")
auth_token = _p("Twilio Auth Token", secret=True)
if not account_sid or not auth_token:
_die("account_sid and auth_token are required.")
sms_from = _p(
"Twilio SMS From number (E.164, e.g. +14155551234; leave blank to use a Messaging Service SID)"
)
messaging_service_sid = ""
if not sms_from:
messaging_service_sid = _p("Twilio Messaging Service SID (starts with MG...)")
if not messaging_service_sid:
_die("SMS requires either a from_number or a messaging_service_sid.")
upsert_integration(
"twilio",
{
"credentials": {
"account_sid": account_sid,
"auth_token": auth_token,
"sms": {
"enabled": True,
"from_number": sms_from,
"messaging_service_sid": messaging_service_sid,
"default_to": _p("Default SMS recipient (optional, E.164)") or None,
},
}
},
)
def _setup_openclaw() -> None:
# Transport is fixed to stdio (the local OpenClaw bridge). In practice it is the
# only mode anyone selects, so the transport prompt was removed on purpose — do
# NOT reintroduce a transport selection or a remote streamable-http/SSE branch.
mode = "stdio"
credentials: dict[str, Any] = {"mode": mode}
command = _p("OpenClaw bridge command", default="openclaw")
args = _p("OpenClaw bridge args", default="mcp serve")
if not command:
_die("command is required for stdio mode.")
credentials["command"] = command
credentials["args"] = [part for part in args.split() if part]
credentials["url"] = ""
credentials["auth_token"] = ""
print("\n Validating OpenClaw bridge...")
config = build_openclaw_config(credentials)
result = validate_openclaw_config(config)
print(f" {result.detail}")
if not result.ok:
sys.exit(1)
upsert_integration("openclaw", {"credentials": credentials})
print(" Next:")
print(" - opensre integrations verify openclaw")
print(" - uv run opensre investigate -i tests/fixtures/openclaw_test_alert.json")
print(" - for accurate RCA, also configure Grafana/Datadog and GitHub")
def _setup_posthog_mcp() -> None:
# Transport is fixed to Streamable HTTP (the hosted PostHog MCP server). In
# practice it is the only mode anyone selects, so the transport prompt was removed
# on purpose — do NOT reintroduce a transport selection or a stdio branch here.
mode = "streamable-http"
credentials: dict[str, Any] = {"mode": mode, "read_only": True}
url = _p("PostHog MCP URL", default=DEFAULT_POSTHOG_MCP_URL)
if not url:
_die("url is required for remote MCP modes.")
credentials["url"] = url
credentials["command"] = ""
credentials["args"] = []
credentials["auth_token"] = _p("PostHog personal API key (MCP Server preset)", secret=True)
if not credentials["auth_token"]:
_die("a personal API key is required for the hosted PostHog MCP server.")
credentials["project_id"] = _p("PostHog project ID (optional)", default="")
print("\n Validating PostHog MCP...")
config = build_posthog_mcp_config(credentials)
result = validate_posthog_mcp_config(config)
print(f" {result.detail}")
if not result.ok:
sys.exit(1)
upsert_integration("posthog_mcp", {"credentials": credentials})
print(" Next:")
print(" - opensre integrations verify posthog_mcp")
def _setup_sentry_mcp() -> None:
# Transport is fixed to Streamable HTTP (the hosted Sentry MCP server). In
# practice it is the only mode anyone selects, so the transport prompt was removed
# on purpose — do NOT reintroduce a transport selection or a stdio branch here.
mode = "streamable-http"
credentials: dict[str, Any] = {"mode": mode}
url = _p("Sentry MCP URL", default=DEFAULT_SENTRY_MCP_URL)
if not url:
_die("url is required for remote MCP modes.")
credentials["url"] = url
credentials["command"] = ""
credentials["args"] = []
credentials["auth_token"] = _p("Sentry user auth token", secret=True)
if not credentials["auth_token"]:
_die("a user auth token is required for the hosted Sentry MCP server.")
credentials["host"] = _p("Self-hosted Sentry host (optional)", default="")
print("\n Validating Sentry MCP...")
config = build_sentry_mcp_config(credentials)
result = validate_sentry_mcp_config(config)
print(f" {result.detail}")
if not result.ok:
sys.exit(1)
upsert_integration("sentry_mcp", {"credentials": credentials})
print(" Next:")
print(" - opensre integrations verify sentry_mcp")
def _setup_x_mcp() -> None:
# X's MCP server (https://github.com/xdevplatform/xmcp) runs locally by
# default, optionally tunneled for remote access — it is not an
# always-on hosted endpoint like PostHog/Sentry's. Streamable HTTP is
# the transport used by both a bare local server and a tunneled one, so
# it stays the default here; do NOT add a transport prompt.
mode = "streamable-http"
credentials: dict[str, Any] = {"mode": mode}
url = _p("X MCP URL", default=DEFAULT_X_MCP_URL)
if not url:
_die("url is required for remote MCP modes.")
credentials["url"] = url
credentials["command"] = ""
credentials["args"] = []
credentials["auth_token"] = _p(
"Auth token for a tunneled/proxied endpoint (optional)", secret=True, default=""
)
credentials["bearer_token"] = ""
print("\n Validating X MCP...")
config = build_x_mcp_config(credentials)
result = validate_x_mcp_config(config)
print(f" {result.detail}")
if not result.ok:
sys.exit(1)
upsert_integration("x_mcp", {"credentials": credentials})
print(" Next:")
print(" - opensre integrations verify x_mcp")
def _setup_postgresql() -> None:
host = _p("Host (e.g. localhost or postgres.example.com)")
database = _p("Database name")
if not host or not database:
_die("host and database are required.")
port = _p("Port", default="5432")
username = _p("Username", default="postgres")
password = _p("Password", secret=True)
ssl_mode_choice = questionary.select(
"SSL mode",
choices=[
questionary.Choice("prefer (recommended)", value="prefer"),
questionary.Choice("require", value="require"),
questionary.Choice("disable", value="disable"),
],
instruction="(use arrow keys)",
).ask()
if ssl_mode_choice is None:
print("\nAborted.")
sys.exit(1)
upsert_integration(
"postgresql",
{
"credentials": {
"host": host,
"port": int(port) if port.isdigit() else 5432,
"database": database,
"username": username or "postgres",
"password": password,
"ssl_mode": ssl_mode_choice,
}
},
)
def _setup_mysql() -> None:
host = _p("Host (e.g. localhost or mysql.example.com)")
database = _p("Database name")
if not host or not database:
_die("host and database are required.")
port = _p("Port", default="3306")
username = _p("Username", default="root")
password = _p("Password", secret=True)
ssl_mode_choice = questionary.select(
"SSL mode",
choices=[
questionary.Choice("preferred (encrypted, no cert verification)", value="preferred"),
questionary.Choice("required", value="required"),
questionary.Choice("disabled", value="disabled"),
],
instruction="(use arrow keys)",
).ask()
if ssl_mode_choice is None:
print("\nAborted.")
sys.exit(1)
upsert_integration(
"mysql",
{
"credentials": {
"host": host,
"port": int(port) if port.isdigit() else 3306,
"database": database,
"username": username or "root",
"password": password,
"ssl_mode": ssl_mode_choice,
}
},
)
def _setup_mongodb_atlas() -> None:
api_public_key = _p("Atlas API public key")
api_private_key = _p("Atlas API private key", secret=True)
project_id = _p("Atlas project ID (group ID)")
base_url = _p("Atlas API base URL", default="https://cloud.mongodb.com/api/atlas/v2")
if not api_public_key or not api_private_key or not project_id:
_die("api_public_key, api_private_key, and project_id are required.")
upsert_integration(
"mongodb_atlas",
{
"credentials": {
"api_public_key": api_public_key,
"api_private_key": api_private_key,
"project_id": project_id,
"base_url": base_url,
}
},
)
def _setup_mariadb() -> None:
host = _p("Host (e.g. db.example.com)")
port = _p("Port", default="3306")
database = _p("Database name")
username = _p("Username")
password = _p("Password", secret=True)
ssl_choice = questionary.select(
"SSL enabled?",
choices=[
questionary.Choice("Yes", value="1"),
questionary.Choice("No", value="0"),
],
instruction="(use arrow keys)",
).ask()
if ssl_choice is None:
print("\nAborted.")
sys.exit(1)
ssl = ssl_choice == "1"
if not host or not database or not username:
_die("host, database, and username are required.")
upsert_integration(
"mariadb",
{
"credentials": {
"host": host,
"port": _parse_port(port),
"database": database,
"username": username,
"password": password,
"ssl": ssl,
}
},
)
def _setup_alertmanager() -> None:
base_url = _p("Alertmanager URL (e.g. http://alertmanager:9093)")
if not base_url:
_die("base_url is required.")
auth_choice = questionary.select(
" Authentication method:",
choices=[
questionary.Choice("None (unauthenticated / internal network)", value="none"),
questionary.Choice("Bearer token (reverse proxy auth)", value="bearer"),
questionary.Choice("Basic auth (username + password)", value="basic"),
],
instruction="(use arrow keys)",
).ask()
if auth_choice is None:
print("\nAborted.")
sys.exit(1)
credentials: dict[str, Any] = {"base_url": base_url}
if auth_choice == "bearer":
bearer_token = _p("Bearer token", secret=True)
if not bearer_token:
_die("Bearer token is required for bearer auth.")
credentials["bearer_token"] = bearer_token
elif auth_choice == "basic":
username = _p("Username")
if not username:
_die("Username is required for basic auth.")
credentials["username"] = username
credentials["password"] = _p("Password", secret=True)
upsert_integration("alertmanager", {"credentials": credentials})
def _setup_signoz() -> None:
url = _p("SigNoz URL (e.g. http://localhost:8080 for local Docker)")
api_key = _p("SigNoz API key (service account key)", secret=True)
if not (url and api_key):
_die("SigNoz URL and API key are required.")
upsert_integration(
"signoz",
{
"credentials": {
"url": url,
"api_key": api_key,
}
},
)
def _setup_jenkins() -> None:
base_url = _p("Jenkins URL (e.g. http://localhost:8080)")
username = _p("Jenkins username")
api_token = _p("Jenkins API token", secret=True)
if not (base_url and username and api_token):
_die("Jenkins URL, username, and API token are required.")
upsert_integration(
"jenkins",
{
"credentials": {
"base_url": base_url,
"username": username,
"api_token": api_token,
}
},
)
def _setup_helm() -> None:
helm_path = _p("Helm binary path or name", default="helm")
if not helm_path:
_die("helm_path is required.")
kube_context = _p(
"Kubernetes context (optional, passed as --kube-context)",
default="",
)
kubeconfig = _p(
"Kubeconfig file path (optional, passed as --kubeconfig)",
default="",
)
default_namespace = _p(
"Default namespace when alerts do not specify one (optional)",
default="",
)
upsert_integration(
"helm",
{
"credentials": {
"helm_path": helm_path,
"kube_context": kube_context,
"kubeconfig": kubeconfig,
"default_namespace": default_namespace,
}
},
)
def _setup_tempo() -> None:
from integrations.tempo import build_tempo_config, validate_tempo_config
url = _p("Tempo URL (e.g. http://localhost:3200 for local Docker)")
if not url:
_die("Tempo URL is required.")
api_key = _p(
"Tempo bearer token (optional, leave blank if using basic auth or none)", secret=True
)
username = _p("Tempo username (optional, for basic auth)")
password = _p("Tempo password (optional, for basic auth)", secret=True)
org_id = _p("Tempo tenant / X-Scope-OrgID (optional, leave blank if single-tenant)")
credentials: dict[str, str] = {"url": url}
if api_key:
credentials["api_key"] = api_key
if username:
credentials["username"] = username
if password:
credentials["password"] = password
if org_id:
credentials["org_id"] = org_id
result = validate_tempo_config(build_tempo_config(credentials))
if not result.ok:
_die(f"Tempo validation failed: {result.detail}")
upsert_integration("tempo", {"credentials": credentials})
def _setup_pagerduty() -> None:
api_key = _p("PagerDuty API key", secret=True)
base_url = _p("API base URL override (optional)")
if not api_key:
_die("api_key is required.")
if not base_url:
base_url = "https://api.pagerduty.com"
upsert_integration(
"pagerduty",
{
"credentials": {
"api_key": api_key,
"base_url": base_url,
}
},
)
_HANDLERS: dict[str, Any] = {
"alertmanager": _setup_alertmanager,
"aws": _setup_aws,
"betterstack": _setup_betterstack,
"coralogix": _setup_coralogix,
"datadog": _setup_datadog,
"groundcover": _setup_groundcover,
"grafana": _setup_grafana,
"honeycomb": _setup_honeycomb,
"helm": _setup_helm,
"incident_io": _setup_incident_io,
"mariadb": _setup_mariadb,
"mongodb_atlas": _setup_mongodb_atlas,
"slack": _setup_slack,
"opensearch": _setup_opensearch,
"rds": _setup_rds,
"tracer": _setup_tracer,
"vercel": _setup_vercel,
"github": _setup_github,
"gitlab": _setup_gitlab,
"sentry": _setup_sentry,
"mongodb": _setup_mongodb,
"discord": _setup_discord,
"telegram": _setup_telegram,
"smtp": _setup_smtp,
"whatsapp": _setup_whatsapp,
"twilio": _setup_twilio,
"openclaw": _setup_openclaw,
"posthog_mcp": _setup_posthog_mcp,
"sentry_mcp": _setup_sentry_mcp,
"x_mcp": _setup_x_mcp,
"postgresql": _setup_postgresql,
"mysql": _setup_mysql,
"redis": _setup_redis,
"signoz": _setup_signoz,
"jenkins": _setup_jenkins,
"tempo": _setup_tempo,
"pagerduty": _setup_pagerduty,
}
def _setup_dagster() -> None:
endpoint = _p(
"Dagster GraphQL endpoint "
"(e.g. http://localhost:3000 or https://<org>.dagster.plus/<deployment>)"
)
if not endpoint:
_die("endpoint is required.")
api_token = _p(
"Dagster Cloud API token (leave empty for local OSS Dagster with no auth)",
secret=True,
)
upsert_integration(
"dagster",
{
"credentials": {
"endpoint": endpoint,
"api_token": api_token,
}
},
)
_HANDLERS["dagster"] = _setup_dagster
def _setup_temporal() -> None:
base_url = _p("Temporal HTTP API base URL (e.g. http://localhost:7243)")
if not base_url:
_die("base_url is required.")
namespace = _p("Temporal namespace", default="default")
api_key = _p(
"Temporal API key (leave empty for unauthenticated self-hosted clusters)",
secret=True,
)
upsert_integration(
"temporal",
{
"credentials": {
"base_url": base_url,
"namespace": namespace or "default",
"api_key": api_key,
}
},
)
_HANDLERS["temporal"] = _setup_temporal
def _setup_azure_sql() -> None:
server = _p("Server (e.g. myserver.database.windows.net)")
database = _p("Database name")
if not server or not database:
_die("server and database are required.")
port = _p("Port", default="1433")
username = _p("Username")
password = _p("Password", secret=True)
driver = _p("ODBC driver", default="ODBC Driver 18 for SQL Server")
encrypt_choice = questionary.select(
"Encrypt connection?",
choices=[
questionary.Choice("Yes (recommended for Azure)", value="1"),
questionary.Choice("No", value="0"),
],
instruction="(use arrow keys)",
).ask()
if encrypt_choice is None:
print("\nAborted.")
sys.exit(1)
encrypt = encrypt_choice == "1"
upsert_integration(
"azure_sql",
{
"credentials": {
"server": server,
"port": _parse_port(port, default=1433),
"database": database,
"username": username,
"password": password,
"driver": driver or "ODBC Driver 18 for SQL Server",
"encrypt": encrypt,
}
},
)
_HANDLERS["azure_sql"] = _setup_azure_sql
_SETUP_SERVICES = tuple(service for service in SUPPORTED_SETUP_SERVICES if service in _HANDLERS)
SUPPORTED = ", ".join(_SETUP_SERVICES)
SUPPORTED_VERIFY = ", ".join(SUPPORTED_VERIFY_SERVICES)
def cmd_setup(service: str | None) -> str:
if not service:
try:
service = questionary.select(
"Which service would you like to set up?",
choices=list(_SETUP_SERVICES),
instruction="(use arrow keys)",
).ask()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
sys.exit(1)
if service:
service = resolve_management_service(service)
if not service or service not in _SETUP_SERVICES:
_die(f"Usage: setup <service>. Supported: {SUPPORTED}")
print(f"\n Setting up {_B}{service}{_R}\n")
_HANDLERS[service]()
print(f"\n ✓ Saved → {STORE_PATH}\n")
return service
def cmd_list() -> None:
from platform.common.runtime_flags import is_json_output
items = list_integrations()
if is_json_output():
_json_echo(items)
return
if not items:
print(
" No integrations. Run: opensre integrations setup <service>, "
"or opensre onboard for the guided wizard."
)
return
print(f"\n {_B}{'SERVICE':<14}STATUS ID{_R}")
for i in items:
print(f" {i['service']:<14}{i['status']:<10}{i['id']}")
print()
def cmd_show(service: str | None) -> None:
if not service:
_die("Usage: show <service>")
return
service = resolve_management_service(service)
record = get_integration(service)
if not record:
_die(f"No active integration for '{service}'.")
return
_json_echo(_mask(record))
def cmd_remove(service: str | None) -> None:
from platform.common.runtime_flags import is_yes
if not service:
_die("Usage: remove <service>")
return
service = resolve_management_service(service)
if not is_yes():
try:
confirmed = questionary.confirm(f" Remove '{service}'?", default=False).ask()
except (EOFError, KeyboardInterrupt):
return
if not confirmed:
print(" Cancelled.")
return
if remove_integration(service):
print(f" ✓ Removed '{service}'.")
else:
print(f" No integration found for '{service}'.")
def cmd_verify(service: str | None, *, send_slack_test: bool = False) -> int:
from platform.common.runtime_flags import is_json_output
if service:
service = resolve_management_service(service)
if service and service not in SUPPORTED_VERIFY_SERVICES:
_die(f"Usage: verify [service]. Supported: {SUPPORTED_VERIFY}")
results = verify_integrations(service=service, send_slack_test=send_slack_test)
if is_json_output():
_json_echo(results)
else:
print(format_verification_results(results))
return verification_exit_code(results, requested_service=service)