From 4f0a5391de5de3e09c846ed8e362a6975c674cdd Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 5 Sep 2026 05:33:59 +0100 Subject: [PATCH 1/2] fix: explain missing connector key at startup --- .../Taskdeck.Api/FirstRun/DesktopRuntime.cs | 14 +++++++++++++ .../FirstRun/DesktopRuntimeTests.cs | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs b/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs index 83c996da9..ee77b6005 100644 --- a/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs +++ b/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs @@ -259,6 +259,20 @@ internal static void WriteRetiredProviderConfigurationIgnored() internal static IReadOnlyList FormatFatalStartup(Exception? exception) { + if (exception is InvalidOperationException validationFailure + && validationFailure.Message.StartsWith( + "SECURITY: The Connectors:EncryptionKey is not configured.", + StringComparison.Ordinal)) + { + return + [ + "TASKDECK_DESKTOP_FATAL code=connector_encryption_key_missing", + "Taskdeck could not start because Connectors__EncryptionKey is missing. Set a stable " + + "base64-encoded 256-bit key (or reuse the existing key for this data) and restart. " + + "No settings were printed." + ]; + } + if (exception is RetiredLlmProviderConfigurationException) { return diff --git a/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs b/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs index dd1607266..d61227775 100644 --- a/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs +++ b/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs @@ -162,6 +162,26 @@ public void FormatFatalStartup_MapsTypedRetiredProviderFailureToStaticActionable Assert.DoesNotContain(exception.Message, output); } + [Fact] + public void FormatFatalStartup_MapsMissingConnectorKeyToStaticActionableOutput() + { + const string secretLikeContent = "synthetic-secret-never-print"; + var exception = new InvalidOperationException( + "SECURITY: The Connectors:EncryptionKey is not configured. " + secretLikeContent); + + var output = DesktopRuntime.FormatFatalStartup(exception); + + Assert.Equal( + [ + "TASKDECK_DESKTOP_FATAL code=connector_encryption_key_missing", + "Taskdeck could not start because Connectors__EncryptionKey is missing. Set a stable " + + "base64-encoded 256-bit key (or reuse the existing key for this data) and restart. " + + "No settings were printed." + ], + output); + Assert.All(output, line => Assert.DoesNotContain(secretLikeContent, line)); + } + [Fact] public void FormatFatalStartup_MapsUnrelatedExceptionToGenericOutputWithoutContentLeak() { From bebfbe98ca4b745a70252b92d526fa0b9233d312 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 5 Sep 2026 14:40:27 +0100 Subject: [PATCH 2/2] fix(desktop): share the missing-connector-key message prefix and document the new fatal code Review round 1 (fresh-context, D-12 sweep): the classifier in DesktopRuntime keyed off a string literal owned by FirstRunBootstrapper with nothing linking the two, so a reword of the producer would silently revert the packaged headless output to code=startup_failed. Both sites and the regression now use FirstRunBootstrapper.MissingConnectorEncryptionKeyMessagePrefix. The Windows quick start troubleshooting list gains the code=connector_encryption_key_missing entry beside the two codes it already documents. --- backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs | 2 +- .../src/Taskdeck.Api/FirstRun/FirstRunBootstrapper.cs | 9 ++++++++- .../Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs | 2 +- docs/releases/WINDOWS_QUICK_START.md | 5 +++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs b/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs index ee77b6005..407ba5f10 100644 --- a/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs +++ b/backend/src/Taskdeck.Api/FirstRun/DesktopRuntime.cs @@ -261,7 +261,7 @@ internal static IReadOnlyList FormatFatalStartup(Exception? exception) { if (exception is InvalidOperationException validationFailure && validationFailure.Message.StartsWith( - "SECURITY: The Connectors:EncryptionKey is not configured.", + FirstRunBootstrapper.MissingConnectorEncryptionKeyMessagePrefix, StringComparison.Ordinal)) { return diff --git a/backend/src/Taskdeck.Api/FirstRun/FirstRunBootstrapper.cs b/backend/src/Taskdeck.Api/FirstRun/FirstRunBootstrapper.cs index 41312bb76..4994db485 100644 --- a/backend/src/Taskdeck.Api/FirstRun/FirstRunBootstrapper.cs +++ b/backend/src/Taskdeck.Api/FirstRun/FirstRunBootstrapper.cs @@ -27,6 +27,13 @@ internal readonly record struct BootstrapIdentityLifecycle( /// public static class FirstRunBootstrapper { + /// + /// Ordinal prefix of the Production missing-connector-key failure. + /// classifies on this exact prefix, so the throw site and the classifier must share it. + /// + internal const string MissingConnectorEncryptionKeyMessagePrefix = + "SECURITY: The Connectors:EncryptionKey is not configured."; + private const string LocalConfigFileName = "appsettings.local.json"; // Placeholder values that indicate "not configured". @@ -648,7 +655,7 @@ public static WebApplicationBuilder ValidateProductionSecrets( if (string.IsNullOrWhiteSpace(connectorKey)) { throw new InvalidOperationException( - "SECURITY: The Connectors:EncryptionKey is not configured. " + + MissingConnectorEncryptionKeyMessagePrefix + " " + "Generate a base64-encoded 256-bit key with 'openssl rand -base64 32' and set it via the " + "Connectors__EncryptionKey environment variable. The application cannot start without a " + $"real encryption key in Production. (If this used to run as a desktop install, an existing " + diff --git a/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs b/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs index d61227775..599c9d372 100644 --- a/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs +++ b/backend/tests/Taskdeck.Api.Tests/FirstRun/DesktopRuntimeTests.cs @@ -167,7 +167,7 @@ public void FormatFatalStartup_MapsMissingConnectorKeyToStaticActionableOutput() { const string secretLikeContent = "synthetic-secret-never-print"; var exception = new InvalidOperationException( - "SECURITY: The Connectors:EncryptionKey is not configured. " + secretLikeContent); + FirstRunBootstrapper.MissingConnectorEncryptionKeyMessagePrefix + " " + secretLikeContent); var output = DesktopRuntime.FormatFatalStartup(exception); diff --git a/docs/releases/WINDOWS_QUICK_START.md b/docs/releases/WINDOWS_QUICK_START.md index ef48f5db0..a2534323e 100644 --- a/docs/releases/WINDOWS_QUICK_START.md +++ b/docs/releases/WINDOWS_QUICK_START.md @@ -180,6 +180,11 @@ created inside Taskdeck, and authenticate local API/MCP clients; never put a `td [UPGRADING.md](../../UPGRADING.md#version-notes). v0.1.2 and later print the accurate diagnostic below instead. - **From v0.3.0-rc.1 onward** — `TASKDECK_DESKTOP_WARNING code=retired_provider_configuration_ignored`: Taskdeck started normally after ignoring retired Gemini settings inherited from this machine's environment (environment variables only — a retired selector passed on the command line still fails closed); no retired value was kept, logged, or printed, and the provider actually in use is the one shown in Taskdeck's provider status. Clear those leftover variables with the commands below when convenient. **The published v0.2.0 archive does not have this behaviour**: there the same leftover variables produce the fatal below, and the commands are a required workaround rather than optional tidying. +- **Unreleased (`main` after 2026-09-05, the build after `v0.3.0-rc.1`)** `TASKDECK_DESKTOP_FATAL + code=connector_encryption_key_missing`: you started the packaged exe with `TASKDECK_HEADLESS` set (the + supply-your-own-key contract) and no `Connectors__EncryptionKey` was configured. Set a stable + base64-encoded 256-bit key, or reuse the key already stored for this data folder, and start again. The + message prints no settings. Earlier builds report this case as the generic `code=startup_failed`. - `TASKDECK_DESKTOP_FATAL code=retired_provider_configuration`: Taskdeck found configuration for the retired Gemini provider and refused to switch providers silently. **In v0.2.0 and earlier this fires for retired Gemini settings from any source, including leftover `Llm__Gemini__*` variables