From 6b14f4c601e0e2a6285aea8a8c6d7935ddff880e Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Tue, 2 Jun 2026 17:37:20 -0400 Subject: [PATCH 1/2] Log previously swallowed exceptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.SetupEngine/Program.cs | 2 +- src/OpenClaw.SetupEngine/SetupDiagnostics.cs | 10 +++ src/OpenClaw.SetupEngine/SetupRunLock.cs | 6 +- src/OpenClaw.SetupEngine/SetupSteps.cs | 14 +++- .../TransactionJournal.cs | 18 +++-- .../A2UI/DataModel/DataModelStore.cs | 38 ++++++++-- .../App.ToastActivation.cs | 31 +++++++- src/OpenClaw.Tray.WinUI/App.xaml.cs | 54 +++++++++++--- .../Chat/OpenClawChatRoot.cs | 10 ++- .../Helpers/CommandCenterTextHelper.cs | 72 +++++++++++++++++++ .../Helpers/LocalizationHelper.cs | 20 +++++- .../Helpers/ThemeHelper.cs | 9 ++- .../Pages/ConnectionPage.xaml.cs | 44 +++++++++--- .../Pages/SettingsPage.xaml.cs | 24 +++++-- .../Services/DeepLinkHandler.cs | 29 ++++---- .../Windows/ChatExplorationsPanelWindow.cs | 4 +- .../Windows/ChatWindow.xaml.cs | 18 +++-- .../DiagnosticsPageContractTests.cs | 12 ++++ 18 files changed, 353 insertions(+), 62 deletions(-) create mode 100644 src/OpenClaw.SetupEngine/SetupDiagnostics.cs diff --git a/src/OpenClaw.SetupEngine/Program.cs b/src/OpenClaw.SetupEngine/Program.cs index 646943d2d..fdbb61b9e 100644 --- a/src/OpenClaw.SetupEngine/Program.cs +++ b/src/OpenClaw.SetupEngine/Program.cs @@ -99,7 +99,7 @@ public static async Task Main(string[] args) // cannot truncate the active run's log or journal files. using var logger = new SetupLogger(config.LogPath, Enum.TryParse(config.LogLevel, true, out var lvl) ? lvl : LogLevel.Trace); var journalPath = Path.ChangeExtension(config.LogPath, ".journal.jsonl"); - using var journal = new TransactionJournal(journalPath); + using var journal = new TransactionJournal(journalPath, logger); var commands = new CommandRunner(logger); using var cts = new CancellationTokenSource(); diff --git a/src/OpenClaw.SetupEngine/SetupDiagnostics.cs b/src/OpenClaw.SetupEngine/SetupDiagnostics.cs new file mode 100644 index 000000000..4dedd21f7 --- /dev/null +++ b/src/OpenClaw.SetupEngine/SetupDiagnostics.cs @@ -0,0 +1,10 @@ +namespace OpenClaw.SetupEngine; + +internal static class SetupDiagnostics +{ + public static void TryWriteStderrWarning(string message) + { + try { Console.Error.WriteLine($"WARN: {message}"); } + catch { } + } +} diff --git a/src/OpenClaw.SetupEngine/SetupRunLock.cs b/src/OpenClaw.SetupEngine/SetupRunLock.cs index af15c7f73..a6b1b0052 100644 --- a/src/OpenClaw.SetupEngine/SetupRunLock.cs +++ b/src/OpenClaw.SetupEngine/SetupRunLock.cs @@ -46,6 +46,10 @@ public static bool TryAcquire(string dataDir, out SetupRunLock? runLock, out str public void Dispose() { _stream.Dispose(); - try { File.Delete(_path); } catch { } + try { File.Delete(_path); } + catch (Exception ex) + { + SetupDiagnostics.TryWriteStderrWarning($"Failed to delete setup lock '{_path}': {ex.Message}"); + } } } diff --git a/src/OpenClaw.SetupEngine/SetupSteps.cs b/src/OpenClaw.SetupEngine/SetupSteps.cs index 69bf34268..09f67d2ae 100644 --- a/src/OpenClaw.SetupEngine/SetupSteps.cs +++ b/src/OpenClaw.SetupEngine/SetupSteps.cs @@ -2638,7 +2638,8 @@ public override Task ExecuteAsync(SetupContext ctx, CancellationToke if (File.Exists(markerPath)) { - try { File.Delete(markerPath); } catch { } + try { File.Delete(markerPath); } + catch (Exception ex) { ctx.Logger.Warn($"Failed to delete stale keepalive marker '{markerPath}': {ex.Message}"); } } // Launch detached keepalive process — keeps the distro alive so port forwarding @@ -2745,7 +2746,10 @@ public override async Task RollbackAsync(SetupContext ctx, CancellationToken ct) ctx.Logger.Info($"[Uninstall] Killed keepalive process tree PID {proc.Id}"); } } - catch { /* process may have exited */ } + catch (Exception ex) + { + ctx.Logger.Debug($"Skipping keepalive process candidate PID {proc.Id}: {ex.Message}"); + } finally { proc.Dispose(); } } } @@ -2803,7 +2807,11 @@ internal static bool IsKeepaliveCommandLine(string? commandLine, string distro) p.WaitForExit(5000); return output.Trim(); } - catch { return null; } + catch (Exception ex) + { + SetupDiagnostics.TryWriteStderrWarning($"Failed to query command line for process {pid}: {ex.Message}"); + return null; + } } } diff --git a/src/OpenClaw.SetupEngine/TransactionJournal.cs b/src/OpenClaw.SetupEngine/TransactionJournal.cs index 0b9efa2bc..4786d505c 100644 --- a/src/OpenClaw.SetupEngine/TransactionJournal.cs +++ b/src/OpenClaw.SetupEngine/TransactionJournal.cs @@ -9,6 +9,7 @@ public sealed class TransactionJournal : IDisposable private readonly StreamWriter? _writer; private readonly List _entries = new(); private readonly object _lock = new(); + private readonly SetupLogger? _logger; public IReadOnlyList Entries { @@ -20,9 +21,10 @@ public IReadOnlyList Entries } public string? FilePath { get; } - public TransactionJournal(string? filePath) + public TransactionJournal(string? filePath, SetupLogger? logger = null) { FilePath = filePath; + _logger = logger; if (filePath != null) { var dir = Path.GetDirectoryName(filePath); @@ -74,6 +76,7 @@ public void RecordPipelineEvent(string eventName, string? detail = null) private void Append(JournalEntry entry) { + IOException? writeFailure = null; lock (_lock) { _entries.Add(entry); @@ -82,11 +85,14 @@ private void Append(JournalEntry entry) var json = JsonSerializer.Serialize(entry, _jsonOptions); _writer?.WriteLine(json); } - catch (IOException) + catch (IOException ex) { - // Journal write failure is non-fatal — entries are still in memory + writeFailure = ex; } } + + if (writeFailure != null) + _logger?.Warn("transaction journal write failed; entries remain in memory", new { file_path = FilePath, error = writeFailure.Message }); } private void LoadExistingEntries(string filePath) @@ -94,8 +100,10 @@ private void LoadExistingEntries(string filePath) if (!File.Exists(filePath)) return; + var lineNumber = 0; foreach (var line in File.ReadLines(filePath)) { + lineNumber++; if (string.IsNullOrWhiteSpace(line)) continue; @@ -105,9 +113,9 @@ private void LoadExistingEntries(string filePath) if (entry != null) _entries.Add(entry); } - catch (JsonException) + catch (JsonException ex) { - // Preserve the journal file as crash evidence even if one line is corrupt. + _logger?.Warn("transaction journal line is corrupt and was skipped", new { file_path = filePath, line = lineNumber, error = ex.Message }); } } } diff --git a/src/OpenClaw.Tray.WinUI/A2UI/DataModel/DataModelStore.cs b/src/OpenClaw.Tray.WinUI/A2UI/DataModel/DataModelStore.cs index adcb3c33c..5499a6df5 100644 --- a/src/OpenClaw.Tray.WinUI/A2UI/DataModel/DataModelStore.cs +++ b/src/OpenClaw.Tray.WinUI/A2UI/DataModel/DataModelStore.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Text.Json.Nodes; using Microsoft.UI.Dispatching; +using OpenClawTray.Services; namespace OpenClawTray.A2UI.DataModel; @@ -95,6 +96,7 @@ public void ApplyDataModelUpdate(string surfaceId, string? basePath, IReadOnlyLi var changed = new List(entries.Count); var prefix = NormalizePath(basePath ?? "/"); if (prefix == "/") prefix = ""; + var droppedEntries = 0; foreach (var entry in entries) { @@ -113,12 +115,17 @@ public void ApplyDataModelUpdate(string surfaceId, string? basePath, IReadOnlyLi model.SetByPointer(pointer, entry.ToJsonNode()); changed.Add(NormalizePath(pointer)); } - catch (Exception) + catch (Exception ex) { - // bad pointer; skip — router logs aggregate. + droppedEntries++; + if (droppedEntries == 1) + Logger.Warn($"Dropped data model entry for surface '{surfaceId}' at key '{entry.Key}': {ex.Message}"); } } + if (droppedEntries > 1) + Logger.Warn($"Dropped {droppedEntries} data model entries for surface '{surfaceId}'."); + if (changed.Count > 0) new DataModelObservable(model, _dispatcher).NotifyPaths(changed); } @@ -314,7 +321,10 @@ public void Write(string pointer, JsonNode? value) _model.SetByPointer(pointer, value); NotifyPaths(new[] { Normalize(pointer) }); } - catch { /* swallow; bad pointer */ } + catch (Exception ex) + { + Logger.Warn($"Failed to write data model pointer '{pointer}': {ex.Message}"); + } } /// @@ -377,8 +387,26 @@ internal void NotifyAllPaths() private void Dispatch(Action callback) { - if (_dispatcher == null || _dispatcher.HasThreadAccess) { try { callback(); } catch { } return; } - _dispatcher.TryEnqueue(() => { try { callback(); } catch { } }); + if (_dispatcher == null || _dispatcher.HasThreadAccess) + { + InvokeSubscriber(callback); + return; + } + + if (!_dispatcher.TryEnqueue(() => InvokeSubscriber(callback))) + Logger.Warn("Data model subscriber callback could not be queued because the dispatcher rejected the work item"); + } + + private static void InvokeSubscriber(Action callback) + { + try + { + callback(); + } + catch (Exception ex) + { + Logger.Warn($"Data model subscriber callback failed: {ex.Message}"); + } } private static string Normalize(string p) => diff --git a/src/OpenClaw.Tray.WinUI/App.ToastActivation.cs b/src/OpenClaw.Tray.WinUI/App.ToastActivation.cs index a4b6a65d2..fb9f40d4e 100644 --- a/src/OpenClaw.Tray.WinUI/App.ToastActivation.cs +++ b/src/OpenClaw.Tray.WinUI/App.ToastActivation.cs @@ -1,4 +1,5 @@ using Microsoft.Toolkit.Uwp.Notifications; +using OpenClaw.Shared; using OpenClawTray.Helpers; using OpenClawTray.Services; using System.Diagnostics; @@ -20,7 +21,10 @@ private void OnToastActivated(ToastNotificationActivatedEventArgsCompat args) OpenUrl = url => { try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } - catch { } + catch (Exception ex) + { + Logger.Warn($"Toast activation failed to open URL '{SanitizeToastUrlForLog(url)}': {ex.Message}"); + } }, OpenDashboard = () => OpenDashboard(), OpenSettings = ShowSettings, @@ -43,6 +47,31 @@ private void OnToastActivated(ToastNotificationActivatedEventArgsCompat args) : null; } + private static string SanitizeToastUrlForLog(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + return string.Empty; + + var sanitized = TokenSanitizer.Sanitize(url.Trim()); + if (!Uri.TryCreate(sanitized, UriKind.Absolute, out var uri)) + return sanitized.Length <= 80 ? sanitized : $"{sanitized[..80]}..."; + + var builder = new UriBuilder(uri) + { + UserName = string.Empty, + Password = string.Empty, + Query = string.Empty, + Fragment = string.Empty + }; + + var safe = builder.Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.SafeUnescaped); + if (!string.IsNullOrEmpty(uri.Query)) + safe += "?[redacted]"; + if (!string.IsNullOrEmpty(uri.Fragment)) + safe += "#[redacted]"; + return safe; + } + public static void CopyTextToClipboard(string text) { ClipboardHelper.CopyText(text); diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs index b99ec4257..3c1117eac 100644 --- a/src/OpenClaw.Tray.WinUI/App.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs @@ -311,7 +311,10 @@ private void OnProcessExit(object? sender, EventArgs e) { Logger.Info($"Process exiting (ExitCode={Environment.ExitCode})"); } - catch { } + catch (Exception ex) + { + Trace.WriteLine($"OpenClaw process-exit logging failed: {ex}"); + } } private static void LogCrash(string source, Exception? ex) @@ -325,7 +328,10 @@ private static void LogCrash(string source, Exception? ex) var message = $"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {source}\n{ex}\n"; File.AppendAllText(CrashLogPath, message); } - catch { /* Can't log the crash logger crash */ } + catch (Exception logFileEx) + { + Trace.WriteLine($"OpenClaw crash file logging failed: {logFileEx}"); + } try { @@ -338,7 +344,10 @@ private static void LogCrash(string source, Exception? ex) Logger.Error($"CRASH {source}"); } } - catch { /* Ignore logging failures */ } + catch (Exception loggerEx) + { + Trace.WriteLine($"OpenClaw crash logger failed: {loggerEx}"); + } } private void OnUiThread(Microsoft.UI.Dispatching.DispatcherQueueHandler action) => _dispatcherQueue?.TryEnqueue(action); @@ -358,7 +367,10 @@ private static void LogCrash(string source, Exception? ex) return protocolArgs.Uri?.ToString(); } } - catch { /* Not activated via protocol, or not packaged */ } + catch (Exception ex) + { + Logger.Debug($"Protocol activation lookup failed or is unavailable: {ex.Message}"); + } return null; } @@ -1080,7 +1092,10 @@ private async Task ExecuteSessionActionAsync(string action, string sessionKey, s .AddText(LocalizationHelper.GetString("Toast_SessionActionFailed")) .AddText(ex.Message)); } - catch { } + catch (Exception toastEx) + { + Logger.Warn($"Failed to show session action failure toast: {toastEx.Message}"); + } } } @@ -2181,7 +2196,10 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status) "node-connected", deviceId); } - catch { /* ignore */ } + catch (Exception ex) + { + Logger.Warn($"Failed to show node-connected toast for device '{deviceId}': {ex.Message}"); + } } } @@ -2212,7 +2230,7 @@ private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatu } else { - Logger.Info($"Suppressing duplicate Paired toast for device {deviceKey}"); + Logger.Info($"Suppressing duplicate Paired toast for device {DeviceIdForLog(deviceKey)}"); } } else if (args.Status == OpenClaw.Shared.PairingStatus.Rejected) @@ -2225,7 +2243,10 @@ private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatu args.DeviceId); } } - catch { /* ignore */ } + catch (Exception ex) + { + Logger.Warn($"Failed to handle pairing status '{args.Status}' for device '{DeviceIdForLog(args.DeviceId)}': {ex.Message}"); + } } /// @@ -2236,6 +2257,18 @@ private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatu public static string BuildPairingApprovalCommand(string deviceId) => $"openclaw devices approve {deviceId}"; + private static string DeviceIdForLog(string? deviceId) + { + if (string.IsNullOrWhiteSpace(deviceId)) + return ""; + + var sanitized = TokenSanitizer.Sanitize(deviceId.Trim()); + if (sanitized.Contains("[REDACTED", StringComparison.Ordinal)) + return sanitized; + + return sanitized.Length <= 8 ? sanitized : $"{sanitized[..8]}..."; + } + public void ShowPairingPendingNotification(string deviceId, string? approvalCommand = null) { var command = approvalCommand ?? BuildPairingApprovalCommand(deviceId); @@ -2762,7 +2795,10 @@ internal void ShowHub(string? navigateTo = null, bool activate = true, string? o } _hubWindow.AppWindow.Show(activateWindow: false); } - catch { /* swallow */ } + catch (Exception ex) + { + Logger.Debug($"Failed to show hub window without activation before tray menu: {ex.Message}"); + } } } diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs index 0f0d8de0f..1a94f5b0c 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs @@ -6,6 +6,7 @@ using OpenClawTray.FunctionalUI; using OpenClawTray.FunctionalUI.Core; using OpenClawTray.Chat.Explorations; +using OpenClawTray.Services; using System; using System.Collections.Generic; using System.Linq; @@ -490,7 +491,14 @@ Element BuildLoadingElement() if (cancelled) return; dq?.TryEnqueue(() => { if (!cancelled) welcomeSettledState.Set(true); }); } - catch { } + catch (OperationCanceledException) + { + // Cancellation is expected when the welcome eligibility signal changes. + } + catch (Exception ex) + { + Logger.Warn($"Welcome-state debounce failed: {ex.Message}"); + } }); return () => { cancelled = true; }; }), diff --git a/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs index 622759d74..6c9b791b5 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -11,6 +12,9 @@ namespace OpenClawTray.Helpers; internal static class CommandCenterTextHelper { + private const int RecentTrayLogTailLines = 120; + private const int RecentTrayLogMaxChars = 24_000; + // Pre-compiled patterns used in RedactSupportPath / RedactSupportValue. // Compiled once at startup; reused on every diagnostic / support-text build. private static readonly Regex PathWindowsUserPattern = new( @@ -120,6 +124,7 @@ internal static string BuildDebugBundle(GatewayCommandCenterState state) AppendSection(builder, "Channel Summary", BuildChannelSummaryText(state.Channels)); AppendSection(builder, "Activity Summary", BuildActivitySummary(state.RecentActivity)); AppendSection(builder, "Extensibility Summary", BuildExtensibilitySummary(state.Channels)); + AppendSection(builder, "Recent Tray Log", BuildRecentTrayLogTail(Logger.LogFilePath)); return builder.ToString(); } @@ -325,6 +330,73 @@ private static void AppendSection(StringBuilder builder, string title, string co builder.AppendLine(); } + private static string BuildRecentTrayLogTail(string? logPath) + { + if (string.IsNullOrWhiteSpace(logPath)) + return "Tray log path is not configured."; + + if (!File.Exists(logPath)) + return $"Tray log does not exist: {RedactSupportPath(logPath)}"; + + var lines = new Queue(RecentTrayLogTailLines); + try + { + using var stream = new FileStream(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var reader = new StreamReader(stream); + while (reader.ReadLine() is { } line) + { + lines.Enqueue(RedactSupportLogLine(line)); + while (lines.Count > RecentTrayLogTailLines) + lines.Dequeue(); + } + } + catch (IOException ex) + { + return $"Unable to read tray log '{RedactSupportPath(logPath)}': {ex.Message}"; + } + catch (UnauthorizedAccessException ex) + { + return $"Unable to read tray log '{RedactSupportPath(logPath)}': {ex.Message}"; + } + + if (lines.Count == 0) + return $"Tray log is empty: {RedactSupportPath(logPath)}"; + + var builder = new StringBuilder(); + builder.AppendLine($"Source: {RedactSupportPath(logPath)}"); + builder.AppendLine($"Showing the last {lines.Count} lines. Secrets and local user paths are redacted."); + foreach (var line in lines) + { + if (builder.Length >= RecentTrayLogMaxChars) + { + builder.AppendLine("... truncated ..."); + break; + } + + builder.AppendLine(line); + } + + return builder.ToString(); + } + + private static string RedactSupportLogLine(string line) + { + var redacted = TokenSanitizer.Sanitize(line); + foreach (var folder in new[] + { + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + }.Where(folder => !string.IsNullOrWhiteSpace(folder)).OrderByDescending(folder => folder.Length)) + { + redacted = redacted.Replace(folder, RedactSupportPath(folder), StringComparison.OrdinalIgnoreCase); + } + + redacted = PathWindowsUserPattern.Replace(redacted, "%USERPROFILE%"); + return PathUnixUserPattern.Replace(redacted, "$HOME"); + } + private static string BuildBrowserProxySshForwardHint(int browserProxyPort, TunnelCommandCenterInfo? tunnel) { if (browserProxyPort is < 1 or > 65535) diff --git a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs index ae8241eea..82408aece 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs @@ -1,5 +1,7 @@ +using System.Collections.Concurrent; using Microsoft.Windows.ApplicationModel.Resources; using OpenClaw.Shared; +using OpenClawTray.Services; namespace OpenClawTray.Helpers; @@ -8,6 +10,9 @@ public static class LocalizationHelper private static ResourceManager? _resourceManager; private static ResourceContext? _overrideContext; private static string? _languageOverride; + private static readonly ConcurrentDictionary s_loggedLookupFailures = new(); + private const int MaxLoggedLookupFailures = 1024; + private static int s_lookupFailureLimitLogged; /// /// Force a specific language for testing (e.g. "zh-CN"). @@ -42,8 +47,20 @@ public static string GetString(string resourceKey) var value = candidate?.ValueAsString; return string.IsNullOrEmpty(value) ? resourceKey : value; } - catch + catch (Exception ex) { + var logKey = $"{_languageOverride ?? ""}:{resourceKey}:{ex.GetType().FullName}"; + if (s_loggedLookupFailures.ContainsKey(logKey)) + return resourceKey; + + if (s_loggedLookupFailures.Count < MaxLoggedLookupFailures && s_loggedLookupFailures.TryAdd(logKey, 0)) + { + Logger.Warn($"Resource lookup failed for '{resourceKey}' (language='{_languageOverride ?? ""}'): {ex.Message}"); + } + else if (System.Threading.Interlocked.Exchange(ref s_lookupFailureLimitLogged, 1) == 0) + { + Logger.Warn("Resource lookup failure log limit reached; suppressing additional unique resource lookup failures"); + } return resourceKey; } } @@ -65,6 +82,7 @@ public static string Format(string resourceKey, params object?[] args) { // Surface the unformatted template instead of crashing. The raw "{0}" // is still useful debugging signal but doesn't kill the page. + Logger.Warn($"Resource format failed for '{resourceKey}': template='{template}'"); return template; } } diff --git a/src/OpenClaw.Tray.WinUI/Helpers/ThemeHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/ThemeHelper.cs index a94927ab1..ca096f1a3 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/ThemeHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/ThemeHelper.cs @@ -1,5 +1,6 @@ using Microsoft.UI.Xaml; using Microsoft.Win32; +using OpenClawTray.Services; using Windows.UI; namespace OpenClawTray.Helpers; @@ -17,8 +18,9 @@ public static bool IsDarkMode() var value = key?.GetValue("AppsUseLightTheme"); return value is int i && i == 0; } - catch + catch (Exception ex) { + Logger.Debug($"Failed to read Windows dark-mode setting: {ex.Message}"); return false; } } @@ -46,7 +48,10 @@ public static Color GetAccentColor() return Color.FromArgb(255, r, g, b); } } - catch { } + catch (Exception ex) + { + Logger.Debug($"Failed to read Windows accent color: {ex.Message}"); + } return Color.FromArgb(255, 0, 120, 212); // #0078D4 — WinUI default accent } diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs index c1ffb3156..3db8d8e29 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs @@ -1863,11 +1863,15 @@ private async Task OnConnectSavedGatewayAsync(object sender) AuthErrorBar.Severity = Microsoft.UI.Xaml.Controls.InfoBarSeverity.Error; AuthErrorBar.IsOpen = true; } - catch { /* last-ditch */ } + catch (Exception uiEx) + { + Logger.Warn($"Failed to surface connect failure in auth error bar: {uiEx.Message}"); + } } finally { - try { btn.IsEnabled = true; } catch { /* control may be detached */ } + try { btn.IsEnabled = true; } + catch (Exception uiEx) { Logger.Debug($"Failed to re-enable connect button; control may be detached: {uiEx.Message}"); } } } @@ -1945,7 +1949,8 @@ private async Task OnSavedRowRemoveAsync(object sender) var wasActive = string.Equals(_gatewayRegistry?.ActiveGatewayId, gwId, StringComparison.Ordinal); if (wasActive && _connectionManager != null) { - try { await _connectionManager.DisconnectAsync(); } catch { } + try { await _connectionManager.DisconnectAsync(); } + catch (Exception ex) { Logger.Warn($"Failed to disconnect active gateway before removal: {ex.Message}"); } } _gatewayRegistry?.Remove(gwId); _gatewayRegistry?.Save(); @@ -2020,9 +2025,14 @@ private async Task RunConnectivityTestAsync(string rawUrl, System.Threading.Canc using var httpClient = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromSeconds(5) }; System.Net.Http.HttpResponseMessage? response = null; + Exception? firstProbeError = null; try { response = await httpClient.GetAsync(httpUrl, ct); } catch (OperationCanceledException) { throw; } - catch { } + catch (Exception ex) + { + firstProbeError = ex; + Logger.Warn($"Gateway connectivity probe failed for {GatewayUrlHelper.SanitizeForDisplay(httpUrl)}: {ex.Message}"); + } if (ct.IsCancellationRequested) return; @@ -2030,7 +2040,11 @@ private async Task RunConnectivityTestAsync(string rawUrl, System.Threading.Canc { try { response = await httpClient.GetAsync($"{httpUrl}/health", ct); } catch (OperationCanceledException) { throw; } - catch { } + catch (Exception ex) + { + Logger.Warn($"Gateway /health connectivity probe failed for {GatewayUrlHelper.SanitizeForDisplay(httpUrl)}: {ex.Message}"); + firstProbeError ??= ex; + } } if (ct.IsCancellationRequested) return; @@ -2047,7 +2061,7 @@ private async Task RunConnectivityTestAsync(string rawUrl, System.Threading.Canc } else { - AddTestResultText.Text = $"✗ Cannot reach gateway"; + AddTestResultText.Text = "✗ Cannot reach gateway. Check the URL and make sure the gateway is running."; AddTestResultText.Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCriticalBrush"]; } } @@ -2056,7 +2070,8 @@ private async Task RunConnectivityTestAsync(string rawUrl, System.Threading.Canc { if (!ct.IsCancellationRequested) { - AddTestResultText.Text = $"✗ {ex.Message}"; + Logger.Warn($"Gateway connectivity test failed for {GatewayUrlHelper.SanitizeForDisplay(rawUrl)}: {ex.Message}"); + AddTestResultText.Text = "✗ Unable to test gateway connection. Check the URL and try again."; AddTestResultText.Foreground = (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources["SystemFillColorCriticalBrush"]; } } @@ -2211,7 +2226,10 @@ private async Task DoDirectConnectFromAddFormAsync() identityBackupMtimeUtc = info.LastWriteTimeUtc; } } - catch { /* backup is best-effort; rollback simply skips restore */ } + catch (Exception ex) + { + Logger.Warn($"Failed to snapshot gateway identity before direct connect; rollback will skip restore: {ex.Message}"); + } if (!string.IsNullOrWhiteSpace(token)) { @@ -2374,7 +2392,10 @@ private void RollbackDirectConnect( File.WriteAllText(identityKeyPath, identityBackup); // else: another writer touched the file; preserve it. } - catch { /* best-effort restore; failure cannot regress further */ } + catch (Exception ex) + { + Logger.Warn($"Failed to restore gateway identity after direct connect rollback: {ex.Message}"); + } } if (settings != null) @@ -3164,7 +3185,10 @@ private static string SanitizeUrl(string url) if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) return uri.Port > 0 ? $"{uri.Scheme}://{uri.Host}:{uri.Port}" : $"{uri.Scheme}://{uri.Host}"; } - catch { } + catch (Exception ex) + { + Logger.Debug($"Failed to sanitize gateway URL '{url}': {ex.Message}"); + } return url; } } diff --git a/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs index 0c727c895..5233e00b7 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs @@ -234,7 +234,10 @@ private void OnTestNotification(object sender, RoutedEventArgs e) .AddText("This is a test notification from OpenClaw settings.") .Show(); } - catch { } + catch (Exception ex) + { + Logger.Warn($"Test notification failed: {ex.Message}"); + } } private void OnRemoveGateway(object sender, RoutedEventArgs e) => @@ -340,13 +343,17 @@ private async Task OnRemoveGatewayAsync() if (doc.RootElement.TryGetProperty("message", out var msg) && msg.GetString() is { Length: > 0 } m) errorMsg = m; } - catch { /* best effort */ } + catch (Exception ex) + { + Logger.Warn($"Failed to parse uninstall result JSON '{jsonOutput}': {ex.Message}"); + } } ShowUninstallError(errorMsg); } // Clean up temp file - try { if (File.Exists(jsonOutput)) File.Delete(jsonOutput); } catch { } + try { if (File.Exists(jsonOutput)) File.Delete(jsonOutput); } + catch (Exception ex) { Logger.Warn($"Failed to delete uninstall result file '{jsonOutput}': {ex.Message}"); } } catch (OperationCanceledException) { @@ -358,7 +365,10 @@ private async Task OnRemoveGatewayAsync() await proc.WaitForExitAsync(CancellationToken.None); } } - catch { /* best effort cancellation cleanup */ } + catch (Exception ex) + { + Logger.Warn($"Failed to stop uninstall process during cancellation: {ex.Message}"); + } ApplyUninstallUiState(UninstallUiState.Failure); UninstallResultBar.Severity = InfoBarSeverity.Warning; @@ -375,7 +385,8 @@ private async Task OnRemoveGatewayAsync() finally { proc?.Dispose(); - try { if (jsonOutput is not null && File.Exists(jsonOutput)) File.Delete(jsonOutput); } catch { } + try { if (jsonOutput is not null && File.Exists(jsonOutput)) File.Delete(jsonOutput); } + catch (Exception ex) { Logger.Warn($"Failed to delete uninstall result file '{jsonOutput}': {ex.Message}"); } _uninstallCts?.Dispose(); _uninstallCts = null; } @@ -390,7 +401,8 @@ private void ShowUninstallError(string message) var viewLogsButton = new Button { Content = "View Logs" }; viewLogsButton.Click += (_, _) => { - try { System.Diagnostics.Process.Start("explorer.exe", logsPath); } catch { } + try { System.Diagnostics.Process.Start("explorer.exe", logsPath); } + catch (Exception ex) { Logger.Warn($"Failed to open logs folder '{logsPath}': {ex.Message}"); } }; UninstallResultBar.Severity = InfoBarSeverity.Error; diff --git a/src/OpenClaw.Tray.WinUI/Services/DeepLinkHandler.cs b/src/OpenClaw.Tray.WinUI/Services/DeepLinkHandler.cs index 537e5653c..4a738772e 100644 --- a/src/OpenClaw.Tray.WinUI/Services/DeepLinkHandler.cs +++ b/src/OpenClaw.Tray.WinUI/Services/DeepLinkHandler.cs @@ -105,7 +105,7 @@ public static void Handle(string uri, DeepLinkActions actions) case "health-check": if (actions.RunHealthCheck != null) { - _ = Task.Run(actions.RunHealthCheck); + _ = RunDeepLinkActionAsync("health check", () => Task.Run(actions.RunHealthCheck)); } break; @@ -115,7 +115,7 @@ public static void Handle(string uri, DeepLinkActions actions) case "update-check": if (actions.CheckForUpdates != null) { - _ = actions.CheckForUpdates(); + _ = RunDeepLinkActionAsync("update check", actions.CheckForUpdates); } break; @@ -229,17 +229,10 @@ public static void Handle(string uri, DeepLinkActions actions) var agentMessage = result.Parameters.GetValueOrDefault("message"); if (!string.IsNullOrEmpty(agentMessage) && actions.SendMessage != null) { - _ = Task.Run(async () => + _ = RunDeepLinkActionAsync("agent message", async () => { - try - { - await actions.SendMessage(agentMessage); - Logger.Info("Sent message via deep link"); - } - catch (Exception ex) - { - Logger.Error($"Failed to send message: {ex.Message}"); - } + await actions.SendMessage(agentMessage); + Logger.Info("Sent message via deep link"); }); } else if (!string.IsNullOrEmpty(agentMessage)) @@ -270,6 +263,18 @@ public static void Handle(string uri, DeepLinkActions actions) break; } } + + private static async Task RunDeepLinkActionAsync(string actionName, Func action) + { + try + { + await action().ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error($"Deep link {actionName} failed: {ex.Message}"); + } + } } public class DeepLinkActions diff --git a/src/OpenClaw.Tray.WinUI/Windows/ChatExplorationsPanelWindow.cs b/src/OpenClaw.Tray.WinUI/Windows/ChatExplorationsPanelWindow.cs index a8dfcff67..83c4be96b 100644 --- a/src/OpenClaw.Tray.WinUI/Windows/ChatExplorationsPanelWindow.cs +++ b/src/OpenClaw.Tray.WinUI/Windows/ChatExplorationsPanelWindow.cs @@ -2,6 +2,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using OpenClawTray.FunctionalUI.Hosting; +using OpenClawTray.Services; using WinUIEx; namespace OpenClawTray.Windows; @@ -30,7 +31,8 @@ public ChatExplorationsPanelWindow() Closed += (_, _) => { - try { _host?.Dispose(); } catch { } + try { _host?.Dispose(); } + catch (Exception ex) { Logger.Warn($"Failed to dispose chat explorations panel host: {ex.Message}"); } _host = null; }; } diff --git a/src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs index ca808cb35..a9998cfda 100644 --- a/src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs +++ b/src/OpenClaw.Tray.WinUI/Windows/ChatWindow.xaml.cs @@ -473,7 +473,8 @@ private void DisposeFunctionalHost() var host = _functionalHost; _functionalHost = null; _mountedProvider = null; - try { host?.Dispose(); } catch { /* tear-down race — non-fatal */ } + try { host?.Dispose(); } + catch (Exception ex) { Logger.Warn($"Failed to dispose chat host during teardown: {ex.Message}"); } } private void EagerlyLoadChatHistory() @@ -493,7 +494,10 @@ private void EagerlyLoadChatHistory() if (snap.DefaultThreadId is { } threadId) await provider.LoadHistoryAsync(threadId); } - catch { /* best effort — the normal mount path will retry */ } + catch (Exception ex) + { + Logger.Warn($"Eager chat history load failed; normal mount path will retry: {ex.Message}"); + } }); } @@ -657,7 +661,10 @@ private async Task ShowAttachmentErrorAsync(string message) }; await dialog.ShowAsync(); } - catch { /* dialog display failed, already logged */ } + catch (Exception ex) + { + Logger.Warn($"Failed to show attachment error dialog: {ex.Message}"); + } } private bool _backdropAppliedOnce; @@ -787,7 +794,10 @@ private void OnPopout(object sender, RoutedEventArgs e) (App.Current as App)?.ShowHub("chat"); this.Hide(); } - catch { } + catch (Exception ex) + { + Logger.Warn($"Failed to pop out chat to hub: {ex.Message}"); + } } private void RequestChatInputFocus() diff --git a/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs b/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs index a0ef2c97a..4f56b5edc 100644 --- a/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs +++ b/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs @@ -545,6 +545,18 @@ public void CommandCenterTextHelper_SupportContext_AdvertisesRedaction() Assert.Contains("bootstrap tokens", helper); } + [Fact] + public void CommandCenterTextHelper_DebugBundle_IncludesSanitizedTrayLogTail() + { + var helper = Read("src", "OpenClaw.Tray.WinUI", "Helpers", "CommandCenterTextHelper.cs"); + Assert.Contains("Recent Tray Log", helper); + Assert.Contains("BuildRecentTrayLogTail(Logger.LogFilePath)", helper); + Assert.Contains("TokenSanitizer.Sanitize(line)", helper); + Assert.Contains("RecentTrayLogTailLines", helper); + Assert.Contains("RecentTrayLogMaxChars", helper); + Assert.Contains("FileShare.ReadWrite | FileShare.Delete", helper); + } + [Fact] public void DebugPage_DetailView_UsesGenerationCounterForRaceSafety() { From 0429f49af86f7fb77cacb14842515f80982907b4 Mon Sep 17 00:00:00 2001 From: Christine Yan Date: Wed, 3 Jun 2026 12:54:38 -0400 Subject: [PATCH 2/2] Sanitize sensitive tray log details before writing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Shared/TokenSanitizer.cs | 75 +++++++++++++++++++ .../Helpers/CommandCenterTextHelper.cs | 4 +- .../Services/AppCrashLogger.cs | 4 +- .../Services/DiagnosticsJsonlService.cs | 2 +- src/OpenClaw.Tray.WinUI/Services/Logger.cs | 2 +- .../TokenSanitizerTests.cs | 24 ++++++ .../DiagnosticsPageContractTests.cs | 15 +++- 7 files changed, 120 insertions(+), 6 deletions(-) diff --git a/src/OpenClaw.Shared/TokenSanitizer.cs b/src/OpenClaw.Shared/TokenSanitizer.cs index e8fa22054..ada5c472e 100644 --- a/src/OpenClaw.Shared/TokenSanitizer.cs +++ b/src/OpenClaw.Shared/TokenSanitizer.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text.RegularExpressions; namespace OpenClaw.Shared; @@ -20,6 +24,38 @@ public static class TokenSanitizer @"(?[^:/\s]+)", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + private static readonly Regex IpAddressPattern = new( + @"\b(?:\d{1,3}\.){3}\d{1,3}\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex EmailPattern = new( + @"\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + private static readonly Regex UserAtHostPattern = new( + @"\b(?[A-Za-z0-9._-]+)@(?[A-Za-z0-9._-]+)(?=[:\s]|$)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex HostAfterToPattern = new( + @"(?<=\bto\s)[A-Za-z0-9._-]+(?=:\d{1,5}\b)", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + private static readonly Regex LeadingHostPattern = new( + @"^\s*[A-Za-z0-9._-]+(?=:\d{1,5}\b)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + public static string Sanitize(string? message) { if (string.IsNullOrEmpty(message)) @@ -32,4 +68,43 @@ public static string Sanitize(string? message) sanitized = BareGatewayHexTokenPattern.Replace(sanitized, "[REDACTED_TOKEN]"); return LongBase64UrlPattern.Replace(sanitized, "[REDACTED_TOKEN]"); } + + public static string SanitizeLogMessage(string? message) + { + var sanitized = Sanitize(message); + if (string.IsNullOrEmpty(sanitized)) + return sanitized; + + sanitized = RedactLocalPaths(sanitized); + sanitized = UrlHostPattern.Replace( + sanitized, + match => match.Value.Replace(match.Groups["host"].Value, "")); + sanitized = IpAddressPattern.Replace(sanitized, ""); + sanitized = EmailPattern.Replace(sanitized, ""); + sanitized = UserAtHostPattern.Replace(sanitized, "@"); + sanitized = HostAfterToPattern.Replace(sanitized, ""); + return LeadingHostPattern.Replace(sanitized, ""); + } + + private static string RedactLocalPaths(string message) + { + var redacted = message; + foreach (var (folder, replacement) in KnownLocalFolders() + .Where(pair => !string.IsNullOrWhiteSpace(pair.Folder)) + .OrderByDescending(pair => pair.Folder.Length)) + { + redacted = redacted.Replace(folder, replacement, StringComparison.OrdinalIgnoreCase); + } + + redacted = PathWindowsUserPattern.Replace(redacted, "%USERPROFILE%"); + return PathUnixUserPattern.Replace(redacted, "$HOME"); + } + + private static IEnumerable<(string Folder, string Replacement)> KnownLocalFolders() + { + yield return (Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "%USERPROFILE%"); + yield return (Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "%APPDATA%"); + yield return (Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "%LOCALAPPDATA%"); + yield return (Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Path.Combine("%USERPROFILE%", "Documents")); + } } diff --git a/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs index 6c9b791b5..8eb64fb49 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/CommandCenterTextHelper.cs @@ -364,7 +364,7 @@ private static string BuildRecentTrayLogTail(string? logPath) var builder = new StringBuilder(); builder.AppendLine($"Source: {RedactSupportPath(logPath)}"); - builder.AppendLine($"Showing the last {lines.Count} lines. Secrets and local user paths are redacted."); + builder.AppendLine($"Showing the last {lines.Count} lines. Sensitive values are redacted before writing and again before bundling."); foreach (var line in lines) { if (builder.Length >= RecentTrayLogMaxChars) @@ -381,7 +381,7 @@ private static string BuildRecentTrayLogTail(string? logPath) private static string RedactSupportLogLine(string line) { - var redacted = TokenSanitizer.Sanitize(line); + var redacted = TokenSanitizer.SanitizeLogMessage(line); foreach (var folder in new[] { Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), diff --git a/src/OpenClaw.Tray.WinUI/Services/AppCrashLogger.cs b/src/OpenClaw.Tray.WinUI/Services/AppCrashLogger.cs index c5d987dc2..06c2bf07d 100644 --- a/src/OpenClaw.Tray.WinUI/Services/AppCrashLogger.cs +++ b/src/OpenClaw.Tray.WinUI/Services/AppCrashLogger.cs @@ -1,3 +1,5 @@ +using OpenClaw.Shared; + namespace OpenClawTray.Services; /// @@ -19,7 +21,7 @@ public void Log(string source, Exception? ex) if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); - var message = $"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {source}\n{ex}\n"; + var message = TokenSanitizer.SanitizeLogMessage($"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {source}\n{ex}\n"); File.AppendAllText(_path, message); } catch { /* Can't log the crash logger crash */ } diff --git a/src/OpenClaw.Tray.WinUI/Services/DiagnosticsJsonlService.cs b/src/OpenClaw.Tray.WinUI/Services/DiagnosticsJsonlService.cs index 5c3bc9dac..5f5ba049a 100644 --- a/src/OpenClaw.Tray.WinUI/Services/DiagnosticsJsonlService.cs +++ b/src/OpenClaw.Tray.WinUI/Services/DiagnosticsJsonlService.cs @@ -72,7 +72,7 @@ public static void Write(string eventName, object metadata) @event = eventName, metadata }; - var line = TokenSanitizer.Sanitize(JsonSerializer.Serialize(record)); + var line = TokenSanitizer.SanitizeLogMessage(JsonSerializer.Serialize(record)); channel.Writer.TryWrite(line); } catch (NotSupportedException ex) diff --git a/src/OpenClaw.Tray.WinUI/Services/Logger.cs b/src/OpenClaw.Tray.WinUI/Services/Logger.cs index f9eb64f67..b5ad4cb0d 100644 --- a/src/OpenClaw.Tray.WinUI/Services/Logger.cs +++ b/src/OpenClaw.Tray.WinUI/Services/Logger.cs @@ -85,7 +85,7 @@ static Logger() private static void Log(string level, string message) { var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); - var line = $"[{timestamp}] [{level}] {TokenSanitizer.Sanitize(message)}"; + var line = $"[{timestamp}] [{level}] {TokenSanitizer.SanitizeLogMessage(message)}"; // TryWrite is non-blocking. With DropOldest semantics the call should // never fail unless the writer has been completed (process shutdown). diff --git a/tests/OpenClaw.Shared.Tests/TokenSanitizerTests.cs b/tests/OpenClaw.Shared.Tests/TokenSanitizerTests.cs index 273b53b8d..7ebdab8e7 100644 --- a/tests/OpenClaw.Shared.Tests/TokenSanitizerTests.cs +++ b/tests/OpenClaw.Shared.Tests/TokenSanitizerTests.cs @@ -228,6 +228,30 @@ public void Sanitize_BearerAndJsonInSameString_BothRedacted() Assert.Contains("[REDACTED]", sanitized); } + [Fact] + public void SanitizeLogMessage_RedactsLocalUserPaths() + { + var sanitized = TokenSanitizer.SanitizeLogMessage(@"Failed reading C:\Users\alice\AppData\Local\OpenClawTray\settings.json"); + + Assert.DoesNotContain("alice", sanitized); + Assert.Contains("%USERPROFILE%", sanitized); + } + + [Fact] + public void SanitizeLogMessage_RedactsNetworkAndIdentityValues() + { + var sanitized = TokenSanitizer.SanitizeLogMessage("Connect to ws://gateway.example.com:19001/ as alice@example.com via 10.1.2.3 and alice@server:22"); + + Assert.DoesNotContain("gateway.example.com", sanitized); + Assert.DoesNotContain("alice@example.com", sanitized); + Assert.DoesNotContain("10.1.2.3", sanitized); + Assert.DoesNotContain("alice@server", sanitized); + Assert.Contains("ws://:19001/", sanitized); + Assert.Contains("", sanitized); + Assert.Contains("", sanitized); + Assert.Contains("@:22", sanitized); + } + private static int CountOccurrences(string source, string pattern) { var count = 0; diff --git a/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs b/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs index 4f56b5edc..fed6b4376 100644 --- a/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs +++ b/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs @@ -551,12 +551,25 @@ public void CommandCenterTextHelper_DebugBundle_IncludesSanitizedTrayLogTail() var helper = Read("src", "OpenClaw.Tray.WinUI", "Helpers", "CommandCenterTextHelper.cs"); Assert.Contains("Recent Tray Log", helper); Assert.Contains("BuildRecentTrayLogTail(Logger.LogFilePath)", helper); - Assert.Contains("TokenSanitizer.Sanitize(line)", helper); + Assert.Contains("TokenSanitizer.SanitizeLogMessage(line)", helper); Assert.Contains("RecentTrayLogTailLines", helper); Assert.Contains("RecentTrayLogMaxChars", helper); Assert.Contains("FileShare.ReadWrite | FileShare.Delete", helper); } + [Fact] + public void TrayLogWriters_SanitizeSensitiveValuesBeforeWriting() + { + var logger = Read("src", "OpenClaw.Tray.WinUI", "Services", "Logger.cs"); + Assert.Contains("TokenSanitizer.SanitizeLogMessage(message)", logger); + + var diagnosticsJsonl = Read("src", "OpenClaw.Tray.WinUI", "Services", "DiagnosticsJsonlService.cs"); + Assert.Contains("TokenSanitizer.SanitizeLogMessage(JsonSerializer.Serialize(record))", diagnosticsJsonl); + + var crashLogger = Read("src", "OpenClaw.Tray.WinUI", "Services", "AppCrashLogger.cs"); + Assert.Contains("TokenSanitizer.SanitizeLogMessage", crashLogger); + } + [Fact] public void DebugPage_DetailView_UsesGenerationCounterForRaceSafety() {