What's wrong
LinuxSecretServiceCredentialStore builds its schema handle in a static field of a nested class (Storage/LinuxSecretServiceCredentialStore.cs, ~line 157):
private static class Schema
{
internal static readonly IntPtr Handle = NativeMethods.secret_schema_new(...);
}
On a machine without libsecret-1.so.0 (a headless server, SSH session, or container), the P/Invoke's DllNotFoundException is thrown inside the static initializer. .NET wraps it in a TypeInitializationException, and every later access to Schema throws that same wrapped exception again.
The class remarks (lines 18–19) tell consumers that the provider "will fail at the first operation; consumers should detect this and fall back to InMemoryCredentialStore". But what they receive is a TypeInitializationException whose message is "The type initializer for 'Schema' threw an exception.", not the DllNotFoundException or CredentialStoreException a caller would reasonably catch.
Repro
In a Linux container with no libsecret (ldconfig -p | grep libsecret is empty):
var store = CredentialStoreFactory.CreateDefault("x");
store.TryLoad(...); // System.TypeInitializationException -> DllNotFoundException: Unable to load shared library 'libsecret-1.so.0'
store.TryLoad(...); // same TypeInitializationException again
Why it matters
Consumers following the documented "detect and fall back" advice miss this case. For example, OAICLI's Auth.IsMissingSecretStore matches PlatformNotSupportedException or DllNotFoundException or EntryPointNotFoundException, so on exactly the headless-Linux setup that fallback was written for, users get Error: The type initializer for 'Schema' threw an exception. and exit code 255 (tracked on the OAICLI side separately).
Suggested fix
- Create the schema lazily inside a method (e.g. a
Lazy<IntPtr> or a guarded static getter), not in a type initializer.
- Catch
DllNotFoundException / EntryPointNotFoundException there and rethrow as CredentialStoreException (or PlatformNotSupportedException) with a message naming libsecret, so there is one documented exception type to catch.
- Update the class remarks to name the exception consumers should catch.
Acceptance criteria
- Without libsecret, the first and every later operation throw the documented exception type, never a
TypeInitializationException.
- A test covers the missing-library path, at least by exercising the translation logic.
What's wrong
LinuxSecretServiceCredentialStorebuilds its schema handle in a static field of a nested class (Storage/LinuxSecretServiceCredentialStore.cs, ~line 157):On a machine without
libsecret-1.so.0(a headless server, SSH session, or container), the P/Invoke'sDllNotFoundExceptionis thrown inside the static initializer. .NET wraps it in aTypeInitializationException, and every later access toSchemathrows that same wrapped exception again.The class remarks (lines 18–19) tell consumers that the provider "will fail at the first operation; consumers should detect this and fall back to
InMemoryCredentialStore". But what they receive is aTypeInitializationExceptionwhose message is "The type initializer for 'Schema' threw an exception.", not theDllNotFoundExceptionorCredentialStoreExceptiona caller would reasonably catch.Repro
In a Linux container with no libsecret (
ldconfig -p | grep libsecretis empty):Why it matters
Consumers following the documented "detect and fall back" advice miss this case. For example, OAICLI's
Auth.IsMissingSecretStorematchesPlatformNotSupportedException or DllNotFoundException or EntryPointNotFoundException, so on exactly the headless-Linux setup that fallback was written for, users getError: The type initializer for 'Schema' threw an exception.and exit code 255 (tracked on the OAICLI side separately).Suggested fix
Lazy<IntPtr>or a guarded static getter), not in a type initializer.DllNotFoundException/EntryPointNotFoundExceptionthere and rethrow asCredentialStoreException(orPlatformNotSupportedException) with a message naming libsecret, so there is one documented exception type to catch.Acceptance criteria
TypeInitializationException.