mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 18:33:15 +00:00
* feat: server-side Datadog/LangSmith observability tools + team creds Add team-wide observability credential settings (Datadog DD_SITE/API/APP keys, LangSmith API key) stored encrypted server-side, with an admin dashboard section to connect/disconnect each provider. When connected, get_agent loads read-only observability tools server-side: Datadog via its hosted MCP server (langchain-mcp-adapters, toolsets=core) and LangSmith read tools (langsmith_get_trace, langsmith_list_runs). Credentials live in the LangGraph server process and are never exposed to the sandbox. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: address review on observability tools Address PR review feedback: - Authorize observability tools per triggering user (admins + the OBSERVABILITY_AUTHORIZED_EMAILS allowlist) so prompt-injected runs from untrusted contributors can't reach team Datadog/LangSmith data. - Use the documented Datadog MCP auth headers DD_API_KEY / DD_APPLICATION_KEY. - Store each provider's credentials under its own store key to avoid a read-modify-write race dropping the other provider on concurrent saves. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: async email resolution in observability authorization gate --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
239 lines
7.4 KiB
Python
239 lines
7.4 KiB
Python
"""Team-wide observability provider credentials (Datadog, LangSmith).
|
|
|
|
Credentials are encrypted at rest with :mod:`agent.encryption` and stored in a
|
|
dedicated LangGraph Store namespace, separate from the plaintext team settings
|
|
record so a settings read never surfaces a secret. They feed the server-side
|
|
Datadog MCP tools and LangSmith read tools — the sandbox never sees these keys.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from langgraph_sdk import get_client
|
|
from pydantic import BaseModel, field_validator
|
|
|
|
from ..encryption import decrypt_token, encrypt_token
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TEAM_CREDENTIALS_NAMESPACE: list[str] = ["team_credentials"]
|
|
DATADOG_KEY = "datadog"
|
|
LANGSMITH_KEY = "langsmith"
|
|
|
|
# Datadog sites that expose a hosted MCP server. The MCP host is derived by
|
|
# swapping the leading ``app.`` (or bare site) for ``mcp.``.
|
|
DEFAULT_DD_SITE = "datadoghq.com"
|
|
SUPPORTED_DD_SITES: frozenset[str] = frozenset(
|
|
{
|
|
"datadoghq.com",
|
|
"us3.datadoghq.com",
|
|
"us5.datadoghq.com",
|
|
"datadoghq.eu",
|
|
"ap1.datadoghq.com",
|
|
"ap2.datadoghq.com",
|
|
}
|
|
)
|
|
|
|
|
|
def _client():
|
|
return get_client()
|
|
|
|
|
|
class DatadogCredentialsUpdate(BaseModel):
|
|
"""Connect Datadog with a scoped API + application key pair."""
|
|
|
|
site: str = DEFAULT_DD_SITE
|
|
api_key: str
|
|
app_key: str
|
|
|
|
@field_validator("site", mode="before")
|
|
@classmethod
|
|
def _normalize_site(cls, v: object) -> str:
|
|
if v is None:
|
|
return DEFAULT_DD_SITE
|
|
if not isinstance(v, str):
|
|
raise ValueError("site must be a string")
|
|
site = v.strip().lower().removeprefix("https://").removeprefix("http://").strip("/")
|
|
site = site.removeprefix("app.")
|
|
if not site:
|
|
return DEFAULT_DD_SITE
|
|
if site not in SUPPORTED_DD_SITES:
|
|
raise ValueError(f"unsupported Datadog site: {site}")
|
|
return site
|
|
|
|
@field_validator("api_key", "app_key")
|
|
@classmethod
|
|
def _require_non_empty(cls, v: object) -> str:
|
|
if not isinstance(v, str) or not v.strip():
|
|
raise ValueError("key must be a non-empty string")
|
|
return v.strip()
|
|
|
|
|
|
class LangSmithCredentialsUpdate(BaseModel):
|
|
"""Connect LangSmith with a read-scoped API key."""
|
|
|
|
api_key: str
|
|
endpoint: str | None = None
|
|
|
|
@field_validator("api_key")
|
|
@classmethod
|
|
def _require_non_empty(cls, v: object) -> str:
|
|
if not isinstance(v, str) or not v.strip():
|
|
raise ValueError("api_key must be a non-empty string")
|
|
return v.strip()
|
|
|
|
@field_validator("endpoint", mode="before")
|
|
@classmethod
|
|
def _normalize_endpoint(cls, v: object) -> str | None:
|
|
if v is None:
|
|
return None
|
|
if not isinstance(v, str):
|
|
raise ValueError("endpoint must be a string")
|
|
endpoint = v.strip().rstrip("/")
|
|
return endpoint or None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DatadogCredentials:
|
|
site: str
|
|
api_key: str
|
|
app_key: str
|
|
|
|
@property
|
|
def mcp_host(self) -> str:
|
|
return f"mcp.{self.site}"
|
|
|
|
def mcp_url(self, toolsets: str) -> str:
|
|
url = f"https://{self.mcp_host}/api/unstable/mcp-server/mcp"
|
|
return f"{url}?toolsets={toolsets}" if toolsets else url
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LangSmithCredentials:
|
|
api_key: str
|
|
endpoint: str
|
|
|
|
|
|
DEFAULT_LANGSMITH_ENDPOINT = "https://api.smith.langchain.com"
|
|
|
|
|
|
def _last4(value: str) -> str:
|
|
return value[-4:] if len(value) >= 4 else value
|
|
|
|
|
|
async def _get_provider(key: str) -> dict[str, Any] | None:
|
|
try:
|
|
item = await _client().store.get_item(TEAM_CREDENTIALS_NAMESPACE, key)
|
|
except Exception as e: # noqa: BLE001
|
|
logger.debug("team credentials lookup failed for %s: %s", key, e)
|
|
return None
|
|
if item is None:
|
|
return None
|
|
value = item.get("value") if isinstance(item, dict) else getattr(item, "value", None)
|
|
return value if isinstance(value, dict) else None
|
|
|
|
|
|
async def _put_provider(key: str, value: dict[str, Any]) -> None:
|
|
await _client().store.put_item(TEAM_CREDENTIALS_NAMESPACE, key, value)
|
|
|
|
|
|
async def _delete_provider(key: str) -> None:
|
|
try:
|
|
await _client().store.delete_item(TEAM_CREDENTIALS_NAMESPACE, key)
|
|
except Exception as e: # noqa: BLE001
|
|
logger.debug("team credentials delete failed for %s: %s", key, e)
|
|
|
|
|
|
async def get_team_credentials_status() -> dict[str, Any]:
|
|
"""Return a redacted, dashboard-safe view of connected providers."""
|
|
datadog = await _get_provider(DATADOG_KEY)
|
|
langsmith = await _get_provider(LANGSMITH_KEY)
|
|
return {
|
|
"datadog": {
|
|
"connected": True,
|
|
"site": datadog.get("site", DEFAULT_DD_SITE),
|
|
"api_key_last4": datadog.get("api_key_last4", ""),
|
|
"updated_at": datadog.get("updated_at"),
|
|
}
|
|
if datadog
|
|
else {"connected": False},
|
|
"langsmith": {
|
|
"connected": True,
|
|
"endpoint": langsmith.get("endpoint", DEFAULT_LANGSMITH_ENDPOINT),
|
|
"api_key_last4": langsmith.get("api_key_last4", ""),
|
|
"updated_at": langsmith.get("updated_at"),
|
|
}
|
|
if langsmith
|
|
else {"connected": False},
|
|
}
|
|
|
|
|
|
async def connect_datadog(update: DatadogCredentialsUpdate) -> dict[str, Any]:
|
|
await _put_provider(
|
|
DATADOG_KEY,
|
|
{
|
|
"site": update.site,
|
|
"encrypted_api_key": encrypt_token(update.api_key),
|
|
"encrypted_app_key": encrypt_token(update.app_key),
|
|
"api_key_last4": _last4(update.api_key),
|
|
"updated_at": datetime.now(UTC).isoformat(),
|
|
},
|
|
)
|
|
return await get_team_credentials_status()
|
|
|
|
|
|
async def disconnect_datadog() -> dict[str, Any]:
|
|
await _delete_provider(DATADOG_KEY)
|
|
return await get_team_credentials_status()
|
|
|
|
|
|
async def connect_langsmith(update: LangSmithCredentialsUpdate) -> dict[str, Any]:
|
|
await _put_provider(
|
|
LANGSMITH_KEY,
|
|
{
|
|
"endpoint": update.endpoint or DEFAULT_LANGSMITH_ENDPOINT,
|
|
"encrypted_api_key": encrypt_token(update.api_key),
|
|
"api_key_last4": _last4(update.api_key),
|
|
"updated_at": datetime.now(UTC).isoformat(),
|
|
},
|
|
)
|
|
return await get_team_credentials_status()
|
|
|
|
|
|
async def disconnect_langsmith() -> dict[str, Any]:
|
|
await _delete_provider(LANGSMITH_KEY)
|
|
return await get_team_credentials_status()
|
|
|
|
|
|
async def get_datadog_credentials() -> DatadogCredentials | None:
|
|
"""Return decrypted Datadog credentials, or ``None`` when not connected."""
|
|
datadog = await _get_provider(DATADOG_KEY)
|
|
if not isinstance(datadog, dict):
|
|
return None
|
|
api_key = decrypt_token(datadog.get("encrypted_api_key", ""))
|
|
app_key = decrypt_token(datadog.get("encrypted_app_key", ""))
|
|
if not api_key or not app_key:
|
|
return None
|
|
return DatadogCredentials(
|
|
site=datadog.get("site", DEFAULT_DD_SITE),
|
|
api_key=api_key,
|
|
app_key=app_key,
|
|
)
|
|
|
|
|
|
async def get_langsmith_credentials() -> LangSmithCredentials | None:
|
|
"""Return decrypted LangSmith credentials, or ``None`` when not connected."""
|
|
langsmith = await _get_provider(LANGSMITH_KEY)
|
|
if not isinstance(langsmith, dict):
|
|
return None
|
|
api_key = decrypt_token(langsmith.get("encrypted_api_key", ""))
|
|
if not api_key:
|
|
return None
|
|
return LangSmithCredentials(
|
|
api_key=api_key,
|
|
endpoint=langsmith.get("endpoint", DEFAULT_LANGSMITH_ENDPOINT),
|
|
)
|