"""Persist ordinary draft OAuth rotation after native external refresh has closed. Admission remains metadata-only. This mutation receives already encrypted native output and updates the exact checked credential in one bounded SQL write, repeating current actor access and comparing the original native read. A revoke, ownership change, deletion or concurrent rotation cannot be silently overwritten or returned as a usable refreshed Tool runtime. Published and Builder confirmed refresh retain their separate existing owners. """ from sqlalchemy import update from sqlalchemy.orm import Session, sessionmaker from core.plugin.entities.plugin_daemon import CredentialType as ToolCredentialType from core.tools.errors import ToolProviderCredentialValidationError from models.credential_permission import CredentialType from models.tools import BuiltinToolProvider from repositories.credentials.query_repository import apply_credential_visibility_filter_for_actor from services.tools.draft_tool_setup import DraftToolRuntimeSelection, tool_credential_fingerprint def persist_draft_tool_refresh( *, session_maker: sessionmaker[Session], selection: DraftToolRuntimeSelection, original_encrypted_credentials: str | None, original_expires_at: int, original_user_id: str | None, refreshed_encrypted_credentials: str, refreshed_expires_at: int, ) -> str: """Commit one authorized unchanged selection and return its opaque version; no external I/O occurs here. The original opaque ciphertext/expiry/owner come from the native constrained read, before OAuth I/O. ``None`` represents the actual stored SQL NULL; its comparison retains SQLAlchemy's IS NULL semantics rather than a blank ciphertext fallback. Conditional UPDATE repeats canonical visibility atomically; a failed compare-and-set rolls back and forbids runtime dispatch. Neither token plaintext nor a graph/client permission marker enters this owner. """ credential = selection.credential if credential is None: raise ToolProviderCredentialValidationError("Tool credential changed or access was revoked during refresh.") statement = update(BuiltinToolProvider).where( BuiltinToolProvider.id == credential.id, BuiltinToolProvider.tenant_id == selection.tenant_id, BuiltinToolProvider.provider == credential.provider, BuiltinToolProvider.credential_type == ToolCredentialType.OAUTH2, BuiltinToolProvider.user_id == original_user_id, BuiltinToolProvider.encrypted_credentials == original_encrypted_credentials, BuiltinToolProvider.expires_at == original_expires_at, ) statement = apply_credential_visibility_filter_for_actor( statement, tenant_id=selection.tenant_id, model_id_column=BuiltinToolProvider.id, model_user_id_column=BuiltinToolProvider.user_id, model_visibility_column=BuiltinToolProvider.visibility, credential_type=CredentialType.BUILTIN_TOOL_PROVIDER, actor_id=selection.actor_id, ) statement = statement.values( encrypted_credentials=refreshed_encrypted_credentials, expires_at=refreshed_expires_at, ).returning(BuiltinToolProvider) try: with session_maker.begin() as session: updated = session.scalar(statement.execution_options(synchronize_session=False)) if updated is None or updated.id != credential.id: raise ToolProviderCredentialValidationError( "Tool credential changed or access was revoked during refresh." ) fingerprint = tool_credential_fingerprint(updated) return fingerprint except ToolProviderCredentialValidationError: raise except Exception: raise ToolProviderCredentialValidationError("Tool credential refresh could not be persisted.") from None