mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-01 15:33:16 +00:00
110 lines
3.4 KiB
Python
110 lines
3.4 KiB
Python
|
|
"""Per-user third-party service credentials (Currents.dev).
|
||
|
|
|
||
|
|
Credentials are encrypted at rest with :mod:`agent.encryption` and stored in a
|
||
|
|
dedicated LangGraph Store namespace, keyed by the user's GitHub login. The
|
||
|
|
sandbox never holds these keys — they feed server-side read-only tools.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
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__)
|
||
|
|
|
||
|
|
USER_CREDENTIALS_NAMESPACE: list[str] = ["user_credentials"]
|
||
|
|
CURRENTS_KEY = "currents"
|
||
|
|
|
||
|
|
CURRENTS_API_BASE = "https://api.currents.dev/v1"
|
||
|
|
|
||
|
|
|
||
|
|
def _client():
|
||
|
|
return get_client()
|
||
|
|
|
||
|
|
|
||
|
|
def _last4(value: str) -> str:
|
||
|
|
return value[-4:] if len(value) >= 4 else value
|
||
|
|
|
||
|
|
|
||
|
|
async def _get_provider(login: str, key: str) -> dict[str, Any] | None:
|
||
|
|
try:
|
||
|
|
item = await _client().store.get_item([*USER_CREDENTIALS_NAMESPACE, login], key)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.debug("user credentials lookup failed for %s/%s: %s", login, 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(login: str, key: str, value: dict[str, Any]) -> None:
|
||
|
|
await _client().store.put_item([*USER_CREDENTIALS_NAMESPACE, login], key, value)
|
||
|
|
|
||
|
|
|
||
|
|
async def _delete_provider(login: str, key: str) -> None:
|
||
|
|
try:
|
||
|
|
await _client().store.delete_item([*USER_CREDENTIALS_NAMESPACE, login], key)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.debug("user credentials delete failed for %s/%s: %s", login, key, e)
|
||
|
|
|
||
|
|
|
||
|
|
class CurrentsCredentialsUpdate(BaseModel):
|
||
|
|
"""Connect Currents.dev with an organization API key."""
|
||
|
|
|
||
|
|
api_key: str
|
||
|
|
|
||
|
|
@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()
|
||
|
|
|
||
|
|
|
||
|
|
async def get_currents_status(login: str) -> dict[str, Any]:
|
||
|
|
"""Return a redacted, dashboard-safe view of the user's Currents key."""
|
||
|
|
currents = await _get_provider(login, CURRENTS_KEY)
|
||
|
|
return {
|
||
|
|
"currents": {
|
||
|
|
"connected": True,
|
||
|
|
"api_key_last4": currents.get("api_key_last4", ""),
|
||
|
|
"updated_at": currents.get("updated_at"),
|
||
|
|
}
|
||
|
|
if currents
|
||
|
|
else {"connected": False},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def connect_currents(login: str, update: CurrentsCredentialsUpdate) -> dict[str, Any]:
|
||
|
|
await _put_provider(
|
||
|
|
login,
|
||
|
|
CURRENTS_KEY,
|
||
|
|
{
|
||
|
|
"encrypted_api_key": encrypt_token(update.api_key),
|
||
|
|
"api_key_last4": _last4(update.api_key),
|
||
|
|
"updated_at": datetime.now(UTC).isoformat(),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
return await get_currents_status(login)
|
||
|
|
|
||
|
|
|
||
|
|
async def disconnect_currents(login: str) -> dict[str, Any]:
|
||
|
|
await _delete_provider(login, CURRENTS_KEY)
|
||
|
|
return await get_currents_status(login)
|
||
|
|
|
||
|
|
|
||
|
|
async def get_currents_api_key(login: str) -> str | None:
|
||
|
|
"""Return the decrypted Currents API key, or ``None`` when not connected."""
|
||
|
|
currents = await _get_provider(login, CURRENTS_KEY)
|
||
|
|
if not isinstance(currents, dict):
|
||
|
|
return None
|
||
|
|
api_key = decrypt_token(currents.get("encrypted_api_key", ""))
|
||
|
|
return api_key or None
|