From 3bcd45d676d97964e6e0da02d5b55f84e1e55c9d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 19 Jul 2026 11:33:56 +0200 Subject: [PATCH] Improved Secret Service diagnostics on Linux (#868) --- .../Assistants/I18N/allTexts.lua | 36 ++++++ .../Pages/Information.razor | 1 + .../plugin.lua | 36 ++++++ .../plugin.lua | 36 ++++++ .../Tools/Rust/DeleteSecretResponse.cs | 3 +- .../Tools/Rust/RequestedSecret.cs | 3 +- .../Tools/Rust/SecretStoreIssueCode.cs | 15 +++ .../Tools/Rust/StoreSecretResponse.cs | 3 +- .../Tools/Services/RustService.APIKeys.cs | 11 +- .../Tools/Services/RustService.Secrets.cs | 40 +++++-- .../wwwroot/changelog/v26.7.3.md | 1 + documentation/Setup.md | 103 ++++++++++++++++-- runtime/Cargo.lock | 1 + runtime/Cargo.toml | 1 + runtime/src/secret.rs | 96 ++++++++++++++++ 15 files changed, 363 insertions(+), 23 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Rust/SecretStoreIssueCode.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 865c2d7d..3fddb9d5 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -7069,6 +7069,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available." +-- On Linux, this library communicates with the FreeDesktop Secret Service. AI Studio uses its structured errors to provide helpful guidance when secure credential storage is unavailable or not configured correctly. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2928990457"] = "On Linux, this library communicates with the FreeDesktop Secret Service. AI Studio uses its structured errors to provide helpful guidance when secure credential storage is unavailable or not configured correctly." + -- Copies the configuration source to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard" @@ -8923,6 +8926,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T8163 -- The generated assistant plugin is invalid. Issue: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T939708112"] = "The generated assistant plugin is invalid. Issue: {0}" +-- The global shortcut could not be registered. The previous shortcut remains active. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "The global shortcut could not be registered. The previous shortcut remains active." + +-- The global shortcut change was cancelled. The previous shortcut remains active. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "The global shortcut change was cancelled. The previous shortcut remains active." + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "The configured transcription provider could not be created." @@ -8968,9 +8977,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T25964655 -- Failed to store the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue." +-- No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1238078807"] = "No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service." + -- Failed to store the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Failed to store the API key due to an API issue." +-- The global shortcut could not be registered because of a desktop integration error. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2032590244"] = "The global shortcut could not be registered because of a desktop integration error." + -- The runtime file manager endpoint returned '{0}'. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2158262203"] = "The runtime file manager endpoint returned '{0}'." @@ -8980,12 +8995,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed -- The runtime file manager endpoint is not available. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2486847754"] = "The runtime file manager endpoint is not available." +-- The global shortcut could not be registered because the desktop service is unavailable. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2944914452"] = "The global shortcut could not be registered because the desktop service is unavailable." + +-- AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3005355097"] = "AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection." + -- The runtime file manager endpoint failed without details. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3082220817"] = "The runtime file manager endpoint failed without details." -- Successfully copied the text to your clipboard UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3351807428"] = "Successfully copied the text to your clipboard" +-- The desktop service returned an invalid response while registering the global shortcut. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3369097283"] = "The desktop service returned an invalid response while registering the global shortcut." + +-- AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3611400673"] = "AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default." + -- Failed to delete the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3658273365"] = "Failed to delete the API key due to an API issue." @@ -8995,9 +9022,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3724548108"] = "Failed -- Failed to get the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3875720022"] = "Failed to get the API key due to an API issue." +-- No saved secret was found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3929880252"] = "No saved secret was found." + -- Failed to get the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T4007657575"] = "Failed to get the secret data due to an API issue." +-- AI Studio could not access secure storage. See the log for technical details. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T624023541"] = "AI Studio could not access secure storage. See the log for technical details." + +-- The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T824858123"] = "The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt." + -- No update found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No update found." diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 7f36c2df..45e47d0d 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -298,6 +298,7 @@ + diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index f43eac29..5f30b912 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -7071,6 +7071,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind noch nicht verfügbar." +-- On Linux, this library communicates with the FreeDesktop Secret Service. AI Studio uses its structured errors to provide helpful guidance when secure credential storage is unavailable or not configured correctly. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2928990457"] = "Unter Linux kommuniziert diese Bibliothek mit dem FreeDesktop Secret Service. AI Studio nutzt dessen strukturierte Fehlermeldungen, um hilfreiche Hinweise zu geben, wenn die sichere Speicherung von Zugangsdaten nicht verfügbar oder nicht korrekt konfiguriert ist." + -- Copies the configuration source to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Kopiert die Quelle der Konfiguration in die Zwischenablage" @@ -8925,6 +8928,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T8163 -- The generated assistant plugin is invalid. Issue: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T939708112"] = "Das generierte Assistenten-Plugin ist ungültig. Problem: {0}" +-- The global shortcut could not be registered. The previous shortcut remains active. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "Die globale Tastenkombination konnte nicht registriert werden. Die vorherige Tastenkombination bleibt aktiv." + +-- The global shortcut change was cancelled. The previous shortcut remains active. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "Die Änderung der globalen Tastenkombination wurde abgebrochen. Die vorherige Tastenkombination bleibt aktiv." + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "Der konfigurierte Transkriptionsanbieter konnte nicht erstellt werden." @@ -8970,9 +8979,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T25964655 -- Failed to store the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems." +-- No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1238078807"] = "Es ist kein kompatibler Dienst zur sicheren Speicherung verfügbar. Richten Sie einen Passwortmanager ein, der den FreeDesktop Secret Service bereitstellt." + -- Failed to store the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Fehler beim Speichern des API-Schlüssels aufgrund eines API-Problems." +-- The global shortcut could not be registered because of a desktop integration error. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2032590244"] = "Die globale Tastenkombination konnte aufgrund eines Fehlers bei der Desktop-Integration nicht registriert werden." + -- The runtime file manager endpoint returned '{0}'. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2158262203"] = "Der Laufzeit-Dateimanager-Endpunkt hat '{0}' zurückgegeben." @@ -8982,12 +8997,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das L -- The runtime file manager endpoint is not available. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2486847754"] = "Der Laufzeit-Dateimanager-Endpunkt ist nicht verfügbar." +-- The global shortcut could not be registered because the desktop service is unavailable. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2944914452"] = "Die globale Tastenkombination konnte nicht registriert werden, da der Desktopdienst nicht verfügbar ist." + +-- AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3005355097"] = "AI Studio konnte nicht auf den sicheren Speicher zugreifen, da die Standardsammlung gesperrt ist. Öffnen Sie Ihren Passwortmanager und entsperren Sie die Standardsammlung." + -- The runtime file manager endpoint failed without details. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3082220817"] = "Der Laufzeit-Dateimanager-Endpunkt ist ohne Details fehlgeschlagen." -- Successfully copied the text to your clipboard UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3351807428"] = "Der Text wurde erfolgreich in die Zwischenablage kopiert." +-- The desktop service returned an invalid response while registering the global shortcut. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3369097283"] = "Der Desktop-Dienst hat beim Registrieren des globalen Tastaturkürzels eine ungültige Antwort zurückgegeben." + +-- AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3611400673"] = "AI Studio konnte nicht auf den sicheren Speicher zugreifen, da keine Standardsammlung konfiguriert ist. Öffnen Sie einen kompatiblen Passwortmanager, erstellen Sie eine Sammlung oder wählen Sie eine aus, entsperren sie und legen Sie diese als Standard fest." + -- Failed to delete the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3658273365"] = "Das API-Schlüssel konnte aufgrund eines API-Problems nicht gelöscht werden." @@ -8997,9 +9024,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3724548108"] = "Der Te -- Failed to get the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3875720022"] = "Der API-Schlüssel konnte aufgrund eines API-Problems nicht abgerufen werden." +-- No saved secret was found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3929880252"] = "Es wurde kein gespeichertes Geheimnis gefunden." + -- Failed to get the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T4007657575"] = "Abrufen der geheimen Daten aufgrund eines API-Problems fehlgeschlagen." +-- AI Studio could not access secure storage. See the log for technical details. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T624023541"] = "AI Studio konnte nicht auf den sicheren Speicher zugreifen. Technische Details finden Sie im Protokoll." + +-- The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T824858123"] = "Die Bestätigung für den sicheren Speicher wurde abgebrochen. Wiederholen Sie den Vorgang und bestätigen Sie die Aufforderung des Passwort-Managers." + -- No update found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "Kein Update gefunden." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 91c8267a..f4ad7fd6 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -7071,6 +7071,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available." +-- On Linux, this library communicates with the FreeDesktop Secret Service. AI Studio uses its structured errors to provide helpful guidance when secure credential storage is unavailable or not configured correctly. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2928990457"] = "On Linux, this library communicates with the FreeDesktop Secret Service. AI Studio uses its structured errors to provide helpful guidance when secure credential storage is unavailable or not configured correctly." + -- Copies the configuration source to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard" @@ -8925,6 +8928,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T8163 -- The generated assistant plugin is invalid. Issue: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGININSTALLSERVICE::T939708112"] = "The generated assistant plugin is invalid. Issue: {0}" +-- The global shortcut could not be registered. The previous shortcut remains active. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T2266307101"] = "The global shortcut could not be registered. The previous shortcut remains active." + +-- The global shortcut change was cancelled. The previous shortcut remains active. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::GLOBALSHORTCUTSERVICE::T3299913860"] = "The global shortcut change was cancelled. The previous shortcut remains active." + -- The configured transcription provider could not be created. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::MEDIATRANSCRIPTIONSERVICE::T1235984176"] = "The configured transcription provider could not be created." @@ -8970,9 +8979,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T25964655 -- Failed to store the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue." +-- No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1238078807"] = "No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service." + -- Failed to store the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Failed to store the API key due to an API issue." +-- The global shortcut could not be registered because of a desktop integration error. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2032590244"] = "The global shortcut could not be registered because of a desktop integration error." + -- The runtime file manager endpoint returned '{0}'. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2158262203"] = "The runtime file manager endpoint returned '{0}'." @@ -8982,12 +8997,24 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed -- The runtime file manager endpoint is not available. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2486847754"] = "The runtime file manager endpoint is not available." +-- The global shortcut could not be registered because the desktop service is unavailable. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2944914452"] = "The global shortcut could not be registered because the desktop service is unavailable." + +-- AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3005355097"] = "AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection." + -- The runtime file manager endpoint failed without details. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3082220817"] = "The runtime file manager endpoint failed without details." -- Successfully copied the text to your clipboard UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3351807428"] = "Successfully copied the text to your clipboard" +-- The desktop service returned an invalid response while registering the global shortcut. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3369097283"] = "The desktop service returned an invalid response while registering the global shortcut." + +-- AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3611400673"] = "AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default." + -- Failed to delete the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3658273365"] = "Failed to delete the API key due to an API issue." @@ -8997,9 +9024,18 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3724548108"] = "Failed -- Failed to get the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3875720022"] = "Failed to get the API key due to an API issue." +-- No saved secret was found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3929880252"] = "No saved secret was found." + -- Failed to get the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T4007657575"] = "Failed to get the secret data due to an API issue." +-- AI Studio could not access secure storage. See the log for technical details. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T624023541"] = "AI Studio could not access secure storage. See the log for technical details." + +-- The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T824858123"] = "The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt." + -- No update found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No update found." diff --git a/app/MindWork AI Studio/Tools/Rust/DeleteSecretResponse.cs b/app/MindWork AI Studio/Tools/Rust/DeleteSecretResponse.cs index 634dc012..8d845ba0 100644 --- a/app/MindWork AI Studio/Tools/Rust/DeleteSecretResponse.cs +++ b/app/MindWork AI Studio/Tools/Rust/DeleteSecretResponse.cs @@ -6,4 +6,5 @@ namespace AIStudio.Tools.Rust; /// True, when the secret was successfully deleted or not found. /// The issue, when the secret could not be deleted. /// True, when the entry was found and deleted. -public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound); \ No newline at end of file +/// The structured issue reported by the native credential store. +public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound, SecretStoreIssueCode IssueCode = SecretStoreIssueCode.NONE); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs b/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs index ce55a784..5fc0cab7 100644 --- a/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs +++ b/app/MindWork AI Studio/Tools/Rust/RequestedSecret.cs @@ -6,4 +6,5 @@ namespace AIStudio.Tools.Rust; /// True, when the secret was successfully retrieved. /// The secret, e.g., API key. /// The issue, when the secret could not be retrieved. -public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue); \ No newline at end of file +/// The structured issue reported by the native credential store. +public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue, SecretStoreIssueCode IssueCode = SecretStoreIssueCode.NONE); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/SecretStoreIssueCode.cs b/app/MindWork AI Studio/Tools/Rust/SecretStoreIssueCode.cs new file mode 100644 index 00000000..2cb087d5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/SecretStoreIssueCode.cs @@ -0,0 +1,15 @@ +namespace AIStudio.Tools.Rust; + +/// +/// A structured issue reported by the native credential store. +/// +public enum SecretStoreIssueCode +{ + NONE, + SECRET_NOT_FOUND, + NO_DEFAULT_COLLECTION, + COLLECTION_LOCKED, + PROMPT_DISMISSED, + SERVICE_UNAVAILABLE, + UNKNOWN, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/StoreSecretResponse.cs b/app/MindWork AI Studio/Tools/Rust/StoreSecretResponse.cs index 04860469..962710e6 100644 --- a/app/MindWork AI Studio/Tools/Rust/StoreSecretResponse.cs +++ b/app/MindWork AI Studio/Tools/Rust/StoreSecretResponse.cs @@ -5,4 +5,5 @@ namespace AIStudio.Tools.Rust; /// /// True, when the secret was successfully stored. /// The issue, when the secret could not be stored. -public readonly record struct StoreSecretResponse(bool Success, string Issue); \ No newline at end of file +/// The structured issue reported by the native credential store. +public readonly record struct StoreSecretResponse(bool Success, string Issue, SecretStoreIssueCode IssueCode = SecretStoreIssueCode.NONE); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs b/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs index 7a9a58e0..b842196f 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.APIKeys.cs @@ -57,9 +57,6 @@ public sealed partial class RustService return legacySecret; } - if (!isTrying) - this.logger!.LogError($"Failed to get the API key for '{secretKey}': '{secret.Issue}'"); - return secret; } @@ -79,8 +76,10 @@ public sealed partial class RustService this.logger!.LogDebug($"Successfully retrieved the API key for '{secretKey}'."); else if (isTrying) this.logger!.LogDebug($"No API key configured for '{secretKey}' (try mode): '{secret.Issue}'"); + else + this.logger!.LogError($"Failed to get the API key for '{secretKey}': '{secret.Issue}'"); - return secret; + return TranslateSecretStoreIssue(secret); } /// @@ -101,7 +100,7 @@ public sealed partial class RustService await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true); } - return state; + return TranslateSecretStoreIssue(state); } private async Task StoreEncryptedAPIKeyByKey(string secretKey, EncryptedText encryptedKey) @@ -161,6 +160,6 @@ public sealed partial class RustService if (!state.Success && !isTrying) this.logger!.LogError($"Failed to delete the API key for '{secretKey}': '{state.Issue}'"); - return state; + return TranslateSecretStoreIssue(state); } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs b/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs index 36ed6b6b..ce29ecd2 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs @@ -4,6 +4,26 @@ namespace AIStudio.Tools.Services; public sealed partial class RustService { + private static string TranslateSecretStoreIssue(SecretStoreIssueCode issueCode, string issue) => issueCode switch + { + SecretStoreIssueCode.NONE => issue, + SecretStoreIssueCode.SECRET_NOT_FOUND => TB("No saved secret was found."), + SecretStoreIssueCode.NO_DEFAULT_COLLECTION => TB("AI Studio could not access secure storage because no default collection is configured. Open a compatible password manager, create or select a collection, unlock it, and set it as the default."), + SecretStoreIssueCode.COLLECTION_LOCKED => TB("AI Studio could not access secure storage because the default collection is locked. Open your password manager and unlock the default collection."), + SecretStoreIssueCode.PROMPT_DISMISSED => TB("The secure-storage confirmation was canceled. Repeat the operation and confirm the password manager prompt."), + SecretStoreIssueCode.SERVICE_UNAVAILABLE => TB("No compatible secure-storage service is available. Configure a password manager that provides the FreeDesktop Secret Service."), + _ => TB("AI Studio could not access secure storage. See the log for technical details."), + }; + + private static StoreSecretResponse TranslateSecretStoreIssue(StoreSecretResponse response) => + response.Success ? response : response with { Issue = TranslateSecretStoreIssue(response.IssueCode, response.Issue) }; + + private static RequestedSecret TranslateSecretStoreIssue(RequestedSecret response) => + response.Success ? response : response with { Issue = TranslateSecretStoreIssue(response.IssueCode, response.Issue) }; + + private static DeleteSecretResponse TranslateSecretStoreIssue(DeleteSecretResponse response) => + response.Success ? response : response with { Issue = TranslateSecretStoreIssue(response.IssueCode, response.Issue) }; + private static string SecretKey(ISecretId secretId, SecretStoreType storeType) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}"; private static string LegacySecretKey(ISecretId secretId) => $"secret::{secretId.SecretId}::{secretId.SecretName}"; @@ -30,9 +50,6 @@ public sealed partial class RustService return legacySecret; } - if (!secret.Success && !isTrying) - this.logger!.LogError($"Failed to get the secret data for '{secretKey}': '{secret.Issue}'"); - return secret; } @@ -62,7 +79,7 @@ public sealed partial class RustService if (state.Success && storeType is SecretStoreType.DATA_SOURCE) await this.DeleteSecretByKey(LegacySecretKey(secretId)); - return state; + return TranslateSecretStoreIssue(state); } /// @@ -95,7 +112,16 @@ public sealed partial class RustService return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue.")); } - return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + var state = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (!state.Success) + { + if (isTrying) + this.logger!.LogDebug($"No secret data configured for '{secretKey}' (try mode): '{state.Issue}'"); + else + this.logger!.LogError($"Failed to get the secret data for '{secretKey}': '{state.Issue}'"); + } + + return TranslateSecretStoreIssue(state); } private async Task DeleteSecretByKey(string secretKey) @@ -112,6 +138,6 @@ public sealed partial class RustService if (!state.Success) this.logger!.LogError($"Failed to delete the secret data for '{secretKey}': '{state.Issue}'"); - return state; + return TranslateSecretStoreIssue(state); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 643f4581..4018b338 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -7,6 +7,7 @@ - Added enterprise configuration options to hide the last changelog and vision panels on the welcome page. Thanks, Dominic Neuburg (`donework`), for the contribution. - Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks. - Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department. +- Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available. - Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience. - Fixed connections to internal HTTPS services and enterprise configuration servers that use organization-provided root certificates on Linux. - Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder. diff --git a/documentation/Setup.md b/documentation/Setup.md index 6b545627..4d398087 100644 --- a/documentation/Setup.md +++ b/documentation/Setup.md @@ -58,15 +58,91 @@ When you are confident in the app's safety, follow these steps: The AI Studio app should now open without any issues. Once the app is installed, it will check for updates automatically. If a new version is available, you will be prompted to install it. ## Linux -MindWork AI Studio is available for modern 64-bit Linux systems. The app is provided as an `AppImage`. We test our app using Ubuntu 22.04 and Raspberry Pi OS 12 (64-bit), but it should work on other distributions as well. +MindWork AI Studio is available for modern 64-bit Linux systems. Starting with release v26.7.3, Flatpak is the recommended installation method. We test AI Studio on Ubuntu 24.04 and 26.04, Kubuntu 24.04, Fedora 43 or newer, and openSUSE Leap 16 or newer, but it should work on other distributions as well. -We have to figure out if you have an Intel/AMD or a modern ARM system on your Linux machine. Open a terminal and run the command `uname -m`. When the output is `x86_64`, you have an Intel/AMD system. When the output is `aarch64`, you have an ARM system. +First, determine whether your system uses the Intel/AMD or ARM architecture: -- **Intel/AMD:** [Download the Intel/AMD AppImage](https://github.com/MindWorkAI/AI-Studio/releases/latest/download/mind-work-ai-studio_amd64.AppImage) of AI Studio. +```bash +uname -m +``` -- **ARM:** [Download the ARM AppImage](https://github.com/MindWorkAI/AI-Studio/releases/latest/download/mind-work-ai-studio_aarch64.AppImage) of AI Studio. +`x86_64` means Intel/AMD; `aarch64` means ARM. -### AppImage Installation +### Recommended: Flatpak Installation + +On Ubuntu, install Flatpak first: + +```bash +sudo apt update +sudo apt install flatpak +``` + +For other Linux distributions, follow the [official Flatpak setup instructions](https://flatpak.org/setup/). + +Open the [latest AI Studio release](https://github.com/MindWorkAI/AI-Studio/releases/latest) and download the bundle for your architecture: + +- **Intel/AMD (`x86_64`):** `MindWork.AI.Studio_x86_64.flatpak` +- **ARM (`aarch64`):** `MindWork.AI.Studio_aarch64.flatpak` + +Install the downloaded bundle for your user account. For Intel/AMD, run: + +```bash +cd ~/Downloads +flatpak install --user ./MindWork.AI.Studio_x86_64.flatpak +``` + +For ARM, run: + +```bash +cd ~/Downloads +flatpak install --user ./MindWork.AI.Studio_aarch64.flatpak +``` + +Confirm the installation of the required GNOME runtime from Flathub when Flatpak asks for it. + +#### Pandoc Extension (Strongly Recommended) + +Pandoc is required for essential file features, including regular file attachments in chats, importing and converting Office documents, and other document-based functionality. We therefore strongly recommend installing the Pandoc extension. AI Studio checks whether a compatible Pandoc version is already available. + +For Intel/AMD, download `MindWork.AI.Studio.Plugin.Pandoc_x86_64.flatpak` and run: + +```bash +cd ~/Downloads +flatpak install --user ./MindWork.AI.Studio.Plugin.Pandoc_x86_64.flatpak +``` + +For ARM, download `MindWork.AI.Studio.Plugin.Pandoc_aarch64.flatpak` and run: + +```bash +cd ~/Downloads +flatpak install --user ./MindWork.AI.Studio.Plugin.Pandoc_aarch64.flatpak +``` + +#### Starting and Updating the Flatpak + +Start AI Studio from your application menu or run: + +```bash +flatpak run org.MindWorkAI.AIStudio +``` + +If no application-menu entry appears, sign out of your desktop session completely and sign in again, or restart the system. + +Until AI Studio is published on Flathub, bundles installed from GitHub do not receive automatic app updates. Download each new bundle and reinstall it. For Intel/AMD, run: + +```bash +cd ~/Downloads +flatpak install --user --reinstall ./MindWork.AI.Studio_x86_64.flatpak +``` + +Use `MindWork.AI.Studio_aarch64.flatpak` instead on ARM. + +### Alternative: AppImage Installation + +If you prefer not to use Flatpak, AI Studio is also available as an AppImage: + +- **Intel/AMD:** [Download the Intel/AMD AppImage](https://github.com/MindWorkAI/AI-Studio/releases/latest/download/mind-work-ai-studio_amd64.AppImage). +- **ARM:** [Download the ARM AppImage](https://github.com/MindWorkAI/AI-Studio/releases/latest/download/mind-work-ai-studio_aarch64.AppImage). **Prepare the AppImage using the desktop environment:** 1. Download the AppImage from the link above. @@ -81,7 +157,20 @@ We have to figure out if you have an Intel/AMD or a modern ARM system on your Li **Prepare the AppImage using the terminal:** 1. Download the AppImage from the link above. -2. Open a terminal and navigate to the Downloads folder: `cd Downloads`. +2. Open a terminal and navigate to the Downloads folder: `cd ~/Downloads`. 3. Make the AppImage executable: `chmod +x mind-work-ai-studio_amd64.AppImage`. 4. You might want to move the AppImage to a more convenient location, e.g., your home directory: `mv mind-work-ai-studio_amd64.AppImage ~/`. -5. Now you can run the AppImage from your file manager (double-click) or the terminal: `./mind-work-ai-studio_amd64.AppImage`. \ No newline at end of file +5. Now you can run the AppImage from your file manager (double-click) or the terminal: `~/mind-work-ai-studio_amd64.AppImage`. + +Use the `aarch64` file name instead of the `amd64` file name on ARM systems. + +### Secure Storage for API Keys + +On Linux, AI Studio stores API keys through the FreeDesktop Secret Service API. A compatible password manager must provide this service, and it must have an unlocked default collection. AI Studio never creates, selects, unlocks, or changes a password manager's default collection itself. + +Compatible configurations include: + +- GNOME Keyring, which can be managed with an application such as Seahorse. Create a password collection if necessary, unlock it, and choose **Set as default**. +- KeePassXC with Secret Service integration enabled and a database group exposed to the service. Keep the relevant database and group unlocked when AI Studio needs to access secrets. + +Automatic login can prevent GNOME Keyring from being unlocked automatically. If secure storage remains locked after login, unlock the default collection in your password manager. \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index dff35324..8e98f77b 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -4138,6 +4138,7 @@ dependencies = [ "calamine", "cbc 0.2.1", "cfg-if", + "dbus-secret-service", "dbus-secret-service-keyring-store", "file-format", "flexi_logger", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0f507725..46501799 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -74,6 +74,7 @@ apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.12", default-features = false, features = ["tokio", "open_uri", "global_shortcuts"] } dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rust"] } +dbus-secret-service = "4.1.0" webkit2gtk = { version = "2.0.2", features = ["v2_8"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] diff --git a/runtime/src/secret.rs b/runtime/src/secret.rs index c587c4d4..4e02ad65 100644 --- a/runtime/src/secret.rs +++ b/runtime/src/secret.rs @@ -5,6 +5,46 @@ use serde::{Deserialize, Serialize}; use crate::api_token::APIToken; use crate::encryption::{EncryptedText, ENCRYPTION}; +/// A structured issue reported by the native credential store. +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +pub enum SecretStoreIssueCode { + None, + SecretNotFound, + NoDefaultCollection, + CollectionLocked, + PromptDismissed, + ServiceUnavailable, + Unknown, +} + +fn issue_code(error: &KeyringError) -> SecretStoreIssueCode { + if matches!(error, KeyringError::NoEntry) { + return SecretStoreIssueCode::SecretNotFound; + } + + #[cfg(target_os = "linux")] + if let KeyringError::PlatformFailure(error) | KeyringError::NoStorageAccess(error) = error { + if let Some(error) = error.downcast_ref::() { + return secret_service_issue_code(error); + } + } + + SecretStoreIssueCode::Unknown +} + +#[cfg(target_os = "linux")] +fn secret_service_issue_code(error: &dbus_secret_service::Error) -> SecretStoreIssueCode { + use dbus_secret_service::Error; + + match error { + Error::NoResult => SecretStoreIssueCode::NoDefaultCollection, + Error::Locked => SecretStoreIssueCode::CollectionLocked, + Error::Prompt => SecretStoreIssueCode::PromptDismissed, + Error::Unavailable => SecretStoreIssueCode::ServiceUnavailable, + _ => SecretStoreIssueCode::Unknown, + } +} + /// Initializes the native credential store used by keyring-core. pub fn init_secret_store() { cfg_if::cfg_if! { @@ -48,6 +88,7 @@ pub async fn store_secret(_token: APIToken, request: Json) -> Json< return Json(StoreSecretResponse { success: false, issue: format!("Failed to decrypt the text: {e}"), + issue_code: SecretStoreIssueCode::Unknown, }) }, }; @@ -60,6 +101,7 @@ pub async fn store_secret(_token: APIToken, request: Json) -> Json< return Json(StoreSecretResponse { success: false, issue: e.to_string(), + issue_code: issue_code(&e), }); }, }; @@ -70,6 +112,7 @@ pub async fn store_secret(_token: APIToken, request: Json) -> Json< Json(StoreSecretResponse { success: true, issue: String::from(""), + issue_code: SecretStoreIssueCode::None, }) }, @@ -78,6 +121,7 @@ pub async fn store_secret(_token: APIToken, request: Json) -> Json< Json(StoreSecretResponse { success: false, issue: e.to_string(), + issue_code: issue_code(&e), }) }, } @@ -96,6 +140,7 @@ pub struct StoreSecret { pub struct StoreSecretResponse { success: bool, issue: String, + issue_code: SecretStoreIssueCode, } /// Retrieves a secret from the secret store using the operating system's keyring. @@ -113,6 +158,7 @@ pub async fn get_secret(_token: APIToken, request: Json) -> Json< success: false, secret: EncryptedText::new(String::from("")), issue: format!("Failed to create secret entry for '{service}' and user '{user_name}': {e}"), + issue_code: issue_code(&e), }); }, }; @@ -130,6 +176,7 @@ pub async fn get_secret(_token: APIToken, request: Json) -> Json< success: false, secret: EncryptedText::new(String::from("")), issue: format!("Failed to encrypt the secret: {e}"), + issue_code: SecretStoreIssueCode::Unknown, }); }, }; @@ -138,6 +185,7 @@ pub async fn get_secret(_token: APIToken, request: Json) -> Json< success: true, secret: encrypted_secret, issue: String::from(""), + issue_code: SecretStoreIssueCode::None, }) }, @@ -150,6 +198,7 @@ pub async fn get_secret(_token: APIToken, request: Json) -> Json< success: false, secret: EncryptedText::new(String::from("")), issue: format!("Failed to retrieve secret for '{service}' and user '{user_name}': {e}"), + issue_code: issue_code(&e), }) }, } @@ -169,6 +218,7 @@ pub struct RequestedSecret { success: bool, secret: EncryptedText, issue: String, + issue_code: SecretStoreIssueCode, } /// Deletes a secret from the secret store using the operating system's keyring. @@ -183,6 +233,7 @@ pub async fn delete_secret(_token: APIToken, request: Json) -> Js success: false, was_entry_found: false, issue: e.to_string(), + issue_code: issue_code(&e), }); }, }; @@ -195,6 +246,7 @@ pub async fn delete_secret(_token: APIToken, request: Json) -> Js success: true, was_entry_found: true, issue: String::from(""), + issue_code: SecretStoreIssueCode::None, }) }, @@ -204,6 +256,7 @@ pub async fn delete_secret(_token: APIToken, request: Json) -> Js success: true, was_entry_found: false, issue: String::from(""), + issue_code: SecretStoreIssueCode::SecretNotFound, }) } @@ -213,6 +266,7 @@ pub async fn delete_secret(_token: APIToken, request: Json) -> Js success: false, was_entry_found: false, issue: e.to_string(), + issue_code: issue_code(&e), }) }, } @@ -224,4 +278,46 @@ pub struct DeleteSecretResponse { success: bool, was_entry_found: bool, issue: String, + issue_code: SecretStoreIssueCode, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_entry_is_reported_as_secret_not_found() { + assert_eq!(issue_code(&KeyringError::NoEntry), SecretStoreIssueCode::SecretNotFound); + } + + #[test] + fn unrelated_keyring_error_uses_unknown_fallback() { + let error = KeyringError::Invalid("service".to_string(), "invalid".to_string()); + assert_eq!(issue_code(&error), SecretStoreIssueCode::Unknown); + } + + #[test] + fn issue_code_is_included_in_json() { + let response = StoreSecretResponse { + success: false, + issue: "technical details".to_string(), + issue_code: SecretStoreIssueCode::NoDefaultCollection, + }; + let json = serde_json::to_value(response).unwrap(); + + assert_eq!(json["issue_code"], "NoDefaultCollection"); + assert_eq!(json["issue"], "technical details"); + } + + #[cfg(target_os = "linux")] + #[test] + fn secret_service_errors_are_mapped_to_issue_codes() { + use dbus_secret_service::Error; + + assert_eq!(secret_service_issue_code(&Error::NoResult), SecretStoreIssueCode::NoDefaultCollection); + assert_eq!(secret_service_issue_code(&Error::Locked), SecretStoreIssueCode::CollectionLocked); + assert_eq!(secret_service_issue_code(&Error::Prompt), SecretStoreIssueCode::PromptDismissed); + assert_eq!(secret_service_issue_code(&Error::Unavailable), SecretStoreIssueCode::ServiceUnavailable); + assert_eq!(secret_service_issue_code(&Error::Parse), SecretStoreIssueCode::Unknown); + } } \ No newline at end of file