From 0c2522074d7b87000b2f98306363cff2bfdcaa1e Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Tue, 14 Apr 2026 10:34:32 -0300 Subject: [PATCH] fix: narrow exception handling and support migrate_to for broken credentials - Catch only ValueError (decryption errors) instead of broad Exception so NotFoundError and other failures propagate correctly - Support migrate_to parameter in the fallback delete path so linked models can be reassigned instead of always cascade-deleted - Sanitize decryption_error message to not expose raw exception details --- api/routers/credentials.py | 30 +++++++++++++++++++++++------- open_notebook/domain/credential.py | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/api/routers/credentials.py b/api/routers/credentials.py index 38e3a2d..57bbb83 100644 --- a/api/routers/credentials.py +++ b/api/routers/credentials.py @@ -269,7 +269,7 @@ async def delete_credential( try: try: cred = await Credential.get(credential_id) - except Exception as decrypt_err: + except ValueError as decrypt_err: # Credential exists but can't be decrypted (wrong encryption key). # Fall back to direct DB operations for deletion. logger.warning( @@ -277,17 +277,33 @@ async def delete_credential( f"falling back to direct delete: {decrypt_err}" ) - # Delete linked models directly + # Query linked models linked = await repo_query( "SELECT * FROM model WHERE credential = $cred_id", {"cred_id": ensure_record_id(credential_id)}, ) deleted_models = 0 - for model_row in linked: - model_id = str(model_row.get("id", "")) - if model_id: - await repo_delete(model_id) - deleted_models += 1 + + if linked and migrate_to: + # Migrate models to another credential + target_cred = await Credential.get(migrate_to) + for model_row in linked: + model_id = str(model_row.get("id", "")) + if model_id: + await repo_query( + "UPDATE $model_id SET credential = $target_id", + { + "model_id": ensure_record_id(model_id), + "target_id": ensure_record_id(target_cred.id), + }, + ) + elif linked: + # Cascade-delete linked models + for model_row in linked: + model_id = str(model_row.get("id", "")) + if model_id: + await repo_delete(model_id) + deleted_models += 1 # Delete the credential itself await repo_delete(credential_id) diff --git a/open_notebook/domain/credential.py b/open_notebook/domain/credential.py index 26d9142..da82257 100644 --- a/open_notebook/domain/credential.py +++ b/open_notebook/domain/credential.py @@ -152,7 +152,7 @@ class Credential(ObjectModel): name=row.get("name", "Unknown"), provider=row.get("provider", "unknown"), modalities=row.get("modalities", []), - decryption_error=f"Failed to decrypt API key. The encryption key may have changed. Error: {e}", + decryption_error="Failed to decrypt API key. The encryption key may have changed.", ) # Preserve the DB id, created, updated from the raw row if row.get("id"):