Files
hive/core/framework/credentials/aden/provider.py
T
Hundao 589c5b06fe fix: resolve all ruff lint and format errors across codebase (#7058)
- Auto-fixed 70 lint errors (import sorting, aliased errors, datetime.UTC)
- Fixed 85 remaining errors manually:
  - E501: wrapped long lines in queen_profiles, catalog, routes_credentials
  - F821: added missing TYPE_CHECKING imports for AgentHost, ToolRegistry,
    HookContext, HookResult; added runtime imports where needed
  - F811: removed duplicate method definitions in queen_lifecycle_tools
  - F841/B007: removed unused variables in discovery.py
  - W291: removed trailing whitespace in queen nodes
  - E402: moved import to top of queen_memory_v2.py
  - Fixed AgentRuntime -> AgentHost in example template type annotations
- Reformatted 343 files with ruff format
2026-04-16 19:30:01 +08:00

440 lines
14 KiB
Python

"""
Aden Sync Provider.
Provider that synchronizes credentials with the Aden authentication server.
The Aden server is the authoritative source for OAuth2 tokens - this provider
fetches and caches tokens locally while delegating refresh operations to Aden.
Usage:
from core.framework.credentials import CredentialStore
from core.framework.credentials.storage import EncryptedFileStorage
from core.framework.credentials.aden import (
AdenCredentialClient,
AdenClientConfig,
AdenSyncProvider,
)
# Configure client (API key loaded from ADEN_API_KEY env var)
client = AdenCredentialClient(AdenClientConfig(
base_url=os.environ["ADEN_API_URL"],
))
# Create provider
provider = AdenSyncProvider(client=client)
# Create store
store = CredentialStore(
storage=EncryptedFileStorage(),
providers=[provider],
auto_refresh=True,
)
# Initial sync from Aden
provider.sync_all(store)
# Use normally - auto-refreshes via Aden when needed
token = store.get_key("hubspot", "access_token")
"""
from __future__ import annotations
import logging
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING
from pydantic import SecretStr
from ..models import CredentialKey, CredentialObject, CredentialRefreshError, CredentialType
from ..provider import CredentialProvider
from .client import (
AdenClientError,
AdenCredentialClient,
AdenCredentialResponse,
AdenRefreshError,
)
if TYPE_CHECKING:
from ..store import CredentialStore
logger = logging.getLogger(__name__)
class AdenSyncProvider(CredentialProvider):
"""
Provider that synchronizes credentials with the Aden server.
The Aden server handles OAuth2 authorization flows and maintains
refresh tokens. This provider:
- Fetches access tokens from the Aden server
- Delegates token refresh to the Aden server
- Caches tokens locally in the credential store
- Optionally reports usage statistics back to Aden
Key benefits:
- Client secrets never leave the Aden server
- Refresh token security (stored only on Aden)
- Centralized audit logging
- Multi-tenant support
Usage:
client = AdenCredentialClient(AdenClientConfig(
base_url="https://api.adenhq.com",
api_key=os.environ["ADEN_API_KEY"],
))
provider = AdenSyncProvider(client=client)
store = CredentialStore(
storage=EncryptedFileStorage(),
providers=[provider],
auto_refresh=True,
)
"""
def __init__(
self,
client: AdenCredentialClient,
provider_id: str = "aden_sync",
refresh_buffer_minutes: int = 5,
report_usage: bool = False,
):
"""
Initialize the Aden sync provider.
Args:
client: Configured Aden API client.
provider_id: Unique identifier for this provider instance.
Useful for multi-tenant scenarios (e.g., 'aden_tenant_123').
refresh_buffer_minutes: Minutes before expiry to trigger refresh.
Default is 5 minutes.
report_usage: Whether to report usage statistics to Aden server.
"""
self._client = client
self._provider_id = provider_id
self._refresh_buffer = timedelta(minutes=refresh_buffer_minutes)
self._report_usage = report_usage
@property
def provider_id(self) -> str:
"""Unique identifier for this provider."""
return self._provider_id
@property
def supported_types(self) -> list[CredentialType]:
"""Credential types this provider can manage."""
return [CredentialType.OAUTH2, CredentialType.BEARER_TOKEN]
def can_handle(self, credential: CredentialObject) -> bool:
"""
Check if this provider can handle a credential.
Returns True if:
- Credential type is supported (OAUTH2 or BEARER_TOKEN)
- Credential's provider_id matches this provider, OR
- Credential has '_aden_managed' metadata flag
"""
if credential.credential_type not in self.supported_types:
return False
# Check if credential is explicitly linked to this provider
if credential.provider_id == self.provider_id:
return True
# Check for Aden-managed flag in metadata
aden_flag = credential.keys.get("_aden_managed")
if aden_flag and aden_flag.value.get_secret_value() == "true":
return True
return False
def refresh(self, credential: CredentialObject) -> CredentialObject:
"""
Refresh credential by requesting new token from Aden server.
The Aden server handles the actual OAuth2 refresh token flow.
This method simply fetches the result.
Args:
credential: The credential to refresh.
Returns:
Updated credential with new access token.
Raises:
CredentialRefreshError: If refresh fails.
"""
try:
# Request Aden to refresh the token
aden_response = self._client.request_refresh(credential.id)
# Update credential with new values
credential = self._update_credential_from_aden(credential, aden_response)
logger.info(f"Refreshed credential '{credential.id}' via Aden server")
# Report usage if enabled
if self._report_usage:
self._client.report_usage(
integration_id=credential.id,
operation="token_refresh",
status="success",
)
return credential
except AdenRefreshError as e:
logger.error(f"Aden refresh failed for '{credential.id}': {e}")
if e.requires_reauthorization:
raise CredentialRefreshError(
f"Integration '{credential.id}' requires re-authorization. "
f"Visit: {e.reauthorization_url or 'your Aden dashboard'}"
) from e
raise CredentialRefreshError(f"Failed to refresh credential '{credential.id}': {e}") from e
except AdenClientError as e:
logger.error(f"Aden client error for '{credential.id}': {e}")
# Check if local token is still valid
access_key = credential.keys.get("access_token")
if access_key and access_key.expires_at:
if datetime.now(UTC) < access_key.expires_at:
logger.warning(f"Aden unavailable, using cached token for '{credential.id}'")
return credential
raise CredentialRefreshError(f"Aden server unavailable and token expired for '{credential.id}'") from e
def validate(self, credential: CredentialObject) -> bool:
"""
Validate credential via Aden server introspection.
Args:
credential: The credential to validate.
Returns:
True if credential is valid.
"""
try:
result = self._client.validate_token(credential.id)
return result.get("valid", False)
except AdenClientError:
# Fall back to local validation
access_key = credential.keys.get("access_token")
if access_key is None:
return False
if access_key.expires_at is None:
# No expiration - assume valid
return True
return datetime.now(UTC) < access_key.expires_at
def should_refresh(self, credential: CredentialObject) -> bool:
"""
Check if credential should be refreshed.
Returns True if access_token is expired or within the refresh buffer.
Args:
credential: The credential to check.
Returns:
True if credential should be refreshed.
"""
access_key = credential.keys.get("access_token")
if access_key is None:
return False
if access_key.expires_at is None:
return False
# Refresh if within buffer of expiration
return datetime.now(UTC) >= (access_key.expires_at - self._refresh_buffer)
def fetch_from_aden(self, integration_id: str) -> CredentialObject | None:
"""
Fetch credential directly from Aden server.
Use this for initial population or when local cache is missing.
Args:
integration_id: The integration identifier (e.g., 'hubspot').
Returns:
CredentialObject if found, None otherwise.
Raises:
AdenClientError: For connection failures.
"""
aden_response = self._client.get_credential(integration_id)
if aden_response is None:
return None
return self._aden_response_to_credential(aden_response)
def sync_all(self, store: CredentialStore) -> int:
"""
Sync all credentials from Aden server to local store.
Calls GET /v1/credentials to list integrations, then fetches
access tokens for each active one.
Args:
store: The credential store to populate.
Returns:
Number of credentials synced.
"""
synced = 0
try:
integrations = self._client.list_integrations()
for info in integrations:
if info.status != "active":
logger.warning(f"Skipping connection '{info.alias}': status={info.status}")
continue
try:
cred = self.fetch_from_aden(info.integration_id)
if cred:
store.save_credential(cred)
synced += 1
logger.info(f"Synced credential '{info.alias}' from Aden")
except Exception as e:
logger.warning(f"Failed to sync '{info.alias}': {e}")
except AdenClientError as e:
logger.error(f"Failed to list integrations from Aden: {e}")
return synced
def report_credential_usage(
self,
credential: CredentialObject,
operation: str,
status: str = "success",
metadata: dict | None = None,
) -> None:
"""
Report credential usage to Aden server.
Args:
credential: The credential that was used.
operation: Operation name (e.g., 'api_call').
status: Operation status ('success', 'error').
metadata: Additional metadata.
"""
if self._report_usage:
self._client.report_usage(
integration_id=credential.id,
operation=operation,
status=status,
metadata=metadata or {},
)
def _update_credential_from_aden(
self,
credential: CredentialObject,
aden_response: AdenCredentialResponse,
) -> CredentialObject:
"""Update credential object from Aden response."""
# Update access token
credential.keys["access_token"] = CredentialKey(
name="access_token",
value=SecretStr(aden_response.access_token),
expires_at=aden_response.expires_at,
)
# Update scopes if present
if aden_response.scopes:
credential.keys["scope"] = CredentialKey(
name="scope",
value=SecretStr(" ".join(aden_response.scopes)),
)
# Mark as Aden-managed
credential.keys["_aden_managed"] = CredentialKey(
name="_aden_managed",
value=SecretStr("true"),
)
# Store integration type
credential.keys["_integration_type"] = CredentialKey(
name="_integration_type",
value=SecretStr(aden_response.integration_type),
)
# Store alias (user-set name from Aden platform)
if aden_response.alias:
credential.keys["_alias"] = CredentialKey(
name="_alias",
value=SecretStr(aden_response.alias),
)
# Persist Aden metadata as identity keys
for meta_key, meta_value in (aden_response.metadata or {}).items():
if meta_value and isinstance(meta_value, str):
credential.keys[f"_identity_{meta_key}"] = CredentialKey(
name=f"_identity_{meta_key}",
value=SecretStr(meta_value),
)
# Update timestamps
credential.last_refreshed = datetime.now(UTC)
credential.provider_id = self.provider_id
return credential
def _aden_response_to_credential(
self,
aden_response: AdenCredentialResponse,
) -> CredentialObject:
"""Convert Aden response to CredentialObject."""
keys: dict[str, CredentialKey] = {
"access_token": CredentialKey(
name="access_token",
value=SecretStr(aden_response.access_token),
expires_at=aden_response.expires_at,
),
"_aden_managed": CredentialKey(
name="_aden_managed",
value=SecretStr("true"),
),
"_integration_type": CredentialKey(
name="_integration_type",
value=SecretStr(aden_response.integration_type),
),
}
# Store alias (user-set name from Aden platform)
if aden_response.alias:
keys["_alias"] = CredentialKey(
name="_alias",
value=SecretStr(aden_response.alias),
)
if aden_response.scopes:
keys["scope"] = CredentialKey(
name="scope",
value=SecretStr(" ".join(aden_response.scopes)),
)
# Persist Aden metadata as identity keys
for meta_key, meta_value in (aden_response.metadata or {}).items():
if meta_value and isinstance(meta_value, str):
keys[f"_identity_{meta_key}"] = CredentialKey(
name=f"_identity_{meta_key}",
value=SecretStr(meta_value),
)
return CredentialObject(
id=aden_response.integration_id,
credential_type=CredentialType.OAUTH2,
keys=keys,
provider_id=self.provider_id,
auto_refresh=True,
)