Improved Secret Service diagnostics on Linux (#868)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-07-19 11:33:56 +02:00
1 parent 4cc9d1f9a8
commit 3bcd45d676
15 files changed
+363 -23

No files matched your search

@@ -6,4 +6,5 @@ namespace AIStudio.Tools.Rust;
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);
/// <param name="IssueCode">The structured issue reported by the native credential store.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound, SecretStoreIssueCode IssueCode = SecretStoreIssueCode.NONE);
@@ -6,4 +6,5 @@ namespace AIStudio.Tools.Rust;
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue);
/// <param name="IssueCode">The structured issue reported by the native credential store.</param>
public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue, SecretStoreIssueCode IssueCode = SecretStoreIssueCode.NONE);
@@ -0,0 +1,15 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// A structured issue reported by the native credential store.
/// </summary>
public enum SecretStoreIssueCode
{
NONE,
SECRET_NOT_FOUND,
NO_DEFAULT_COLLECTION,
COLLECTION_LOCKED,
PROMPT_DISMISSED,
SERVICE_UNAVAILABLE,
UNKNOWN,
}
@@ -5,4 +5,5 @@ namespace AIStudio.Tools.Rust;
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);
/// <param name="IssueCode">The structured issue reported by the native credential store.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue, SecretStoreIssueCode IssueCode = SecretStoreIssueCode.NONE);
@@ -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);
}
/// <summary>
@@ -101,7 +100,7 @@ public sealed partial class RustService
await this.DeleteAPIKeyByKey(legacySecretKey, isTrying: true);
}
return state;
return TranslateSecretStoreIssue(state);
}
private async Task<StoreSecretResponse> 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);
}
}
@@ -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);
}
/// <summary>
@@ -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<RequestedSecret>(this.jsonRustSerializerOptions);
var state = await result.Content.ReadFromJsonAsync<RequestedSecret>(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<DeleteSecretResponse> 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);
}
}
}