fix: ConnectionSettings secret migration hardening#383
Conversation
- Save() now returns bool (true on success, false on failure) - RecoverSecretsFromSecureStorage uses Save() return value instead of File.Exists() to guard Keychain cleanup — eliminates data loss if Save() fails but an old settings file exists - Added explanatory comment on ReadSecureStorage's intentional sync-over-async Task.Run pattern - Outer catch in RecoverSecretsFromSecureStorage now logs to Debug.WriteLine and ~/.polypilot/crash.log instead of swallowing silently Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Multi-Model Consensus Review — PR #383 (5 models)Addresses issue #379 (follow-up from PR #377 consensus review). Changes: CI:
|
| Sev | File:Line | Models | Description |
|---|---|---|---|
| 🟢 | ConnectionSettings.cs:274 |
3/5 | File.WriteAllText is truncate-then-write, not atomic. Save() returning true means no exception was thrown, but a crash or power loss mid-write can produce a corrupt/zero-byte settings.json. In theory, Keychain deletion could proceed against a partially-written file. In practice, .NET raises IOException on most partial-write conditions, so this is largely theoretical on Mac Catalyst. A write-to-temp + File.Move(temp, dest, overwrite:true) would make the stated guarantee fully correct. Non-blocking — the PR is strictly better than before. |
| 🟢 | |||
| 🟢 | ConnectionSettingsTests.cs |
2/5 | No tests cover new code paths: Save() returning false (e.g., read-only path), migration skipping Keychain removal when saved==false, or catch branch writing to crash.log. |
Test isolation note (Gemini flagged as HIGH — not a new issue)
GetPolyPilotDir() has always been independent of CopilotService.SetBaseDirForTesting(). The project's existing convention handles this by prohibiting tests from calling ConnectionSettings.Save() or ConnectionSettings.Load() (documented in SessionSidebar.razor instructions). This PR adds a crash.log write via the same pre-existing GetPolyPilotDir(), but only in the #if MACCATALYST migration catch path — which tests don't exercise. Not introduced by this PR.
Issues Addressed from #379
| # | Item | Status |
|---|---|---|
| 1 | File.Exists(SettingsPath) weak proxy |
✅ FIXED — replaced with bool saved = settings.Save() |
| 2 | Sync-over-async comment | ✅ FIXED — comment added to ReadSecureStorage |
| 3 | Silent exception swallowing | ✅ FIXED — catch (Exception ex) with Debug.WriteLine + crash.log |
Recommended Action: ✅ Approve
The core data-loss fix is correct and strictly better than before. All three items from issue #379 are addressed. The consensus findings above are all LOW severity — the File.WriteAllText atomicity concern is theoretical on Mac Catalyst, and the other two are polish items. None are blocking.
Three follow-up hardening items from the PR #377 consensus review. The core risk:
Save()swallowed all exceptions silently,File.Exists()was used as a proxy for write success, and migration failures were invisible.Changes
Save()returnsbool—trueon successful write,falseon any exception. Callers that ignore the return value are unaffected.Keychain cleanup now gated on
Save()success — replaces theFile.Exists(SettingsPath)check, which was unsafe: ifSave()failed (disk full, permissions, etc.) but an older file existed,File.Existswould returntrueand Keychain entries would be deleted without the recovered values persisted.Migration failures logged — outer
catch {}inRecoverSecretsFromSecureStoragechanged tocatch (Exception ex)withDebug.WriteLine+ append to~/.polypilot/crash.log, matching the existing crash-log convention.Sync-over-async comment —
ReadSecureStoragegets an explanatory comment on the intentionalTask.Run(...).GetAwaiter().GetResult()pattern (avoidsSynchronizationContextdeadlock; acceptable for a one-time migration path).Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
4myvsblobprodcus32.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)4vyvsblobprodcus361.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)5dkvsblobprodcus355.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)7tjvsblobprodcus341.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)kijvsblobprodcus387.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)pc2vsblobprodcus360.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)se1vsblobprodcus349.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/55BB33C8E1046D06DB6BC8FB401555C0/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)vb4vsblobprodcus33.vsblob.vsassets.io/usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/8764B2CEA58873189746604E1AFC56D2/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.