update project knowledge function
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""Resolve Jira credentials for an identity without leaking them.
|
||||
|
||||
Credentials come from the existing ``SecretStore`` interface so tests can
|
||||
inject a fake and production uses the OS keyring. The resolver never caches
|
||||
credentials beyond the call scope and never includes them in error messages,
|
||||
logs, or MCP payloads.
|
||||
|
||||
Key naming convention:
|
||||
- Per-project: ``jira:<cowork_project_id>``
|
||||
- Global fallback: ``jira:default``
|
||||
|
||||
The email is stored alongside the token under the same key as a JSON pair
|
||||
``{"email": "...", "api_token": "..."}`` so one secret-store entry carries
|
||||
both values atomically.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from ...infrastructure.secrets.secret_store import SecretStore
|
||||
from ...mcp_servers.project_context.foundation import IdentityContext, ProviderError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class JiraCredentials:
|
||||
"""Immutable credential pair resolved for one call."""
|
||||
email: str
|
||||
api_token: str
|
||||
|
||||
|
||||
def _secret_key(cowork_project_id: str) -> str:
|
||||
return f"jira:{cowork_project_id}"
|
||||
|
||||
|
||||
_GLOBAL_KEY = "jira:default"
|
||||
|
||||
|
||||
class JiraCredentialResolver:
|
||||
"""Resolve ``(email, api_token)`` from the secret store for one identity.
|
||||
|
||||
Raises ``UNAVAILABLE`` when no credentials are configured — never returns
|
||||
empty strings that would cause a silent 401 at the HTTP layer.
|
||||
"""
|
||||
|
||||
def __init__(self, store: SecretStore) -> None:
|
||||
self._store = store
|
||||
|
||||
def resolve(self, identity: IdentityContext) -> JiraCredentials:
|
||||
"""Look up credentials by project-specific key, then global fallback.
|
||||
|
||||
Raises:
|
||||
ProviderError: When neither key exists or the stored value is
|
||||
malformed.
|
||||
"""
|
||||
raw = self._store.get(_secret_key(identity.project))
|
||||
if not raw:
|
||||
raw = self._store.get(_GLOBAL_KEY)
|
||||
if not raw:
|
||||
raise ProviderError(
|
||||
"UNAVAILABLE",
|
||||
"Jira credentials are not configured for this project.",
|
||||
retryable=False,
|
||||
)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
raise ProviderError(
|
||||
"UNAVAILABLE",
|
||||
"Stored Jira credentials are malformed; re-enter them in Connectors.",
|
||||
retryable=False,
|
||||
)
|
||||
if not isinstance(parsed, dict):
|
||||
raise ProviderError(
|
||||
"UNAVAILABLE",
|
||||
"Stored Jira credentials are malformed; re-enter them in Connectors.",
|
||||
retryable=False,
|
||||
)
|
||||
email = str(parsed.get("email", "")).strip()
|
||||
api_token = str(parsed.get("api_token", "")).strip()
|
||||
if not email or not api_token:
|
||||
raise ProviderError(
|
||||
"UNAVAILABLE",
|
||||
"Stored Jira credentials are incomplete; re-enter them in Connectors.",
|
||||
retryable=False,
|
||||
)
|
||||
return JiraCredentials(email=email, api_token=api_token)
|
||||
|
||||
|
||||
__all__ = ["JiraCredentialResolver", "JiraCredentials"]
|
||||
Reference in New Issue
Block a user