Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 144 additions & 12 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using System.IO.Pipes;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -32,8 +33,6 @@

public partial class App : Application, OpenClawTray.Services.IAppCommands
{
private const string PipeName = "OpenClawTray-DeepLink";

internal static readonly UpdatumManager AppUpdater = new("shanselman", "openclaw-windows-hub")
{
FetchOnlyLatestRelease = true,
Expand Down Expand Up @@ -101,7 +100,7 @@
SshTunnelCommandLine.CanForwardBrowserProxyPort(_settings.SshTunnelRemotePort, _settings.SshTunnelLocalPort);
if (_settings.NodeBrowserProxyEnabled && !includeBrowserProxyForward)
{
Logger.Warn("SSH tunnel browser proxy forward disabled because the derived port would be invalid");

Check warning on line 103 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 103 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 103 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 103 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 103 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 103 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.
}

_sshTunnelService.EnsureStarted(
Expand Down Expand Up @@ -266,6 +265,8 @@
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OpenClawTray");
private static readonly string DeepLinkPipeName =
DeepLinkSecurityPolicy.BuildCurrentUserScopedPipeName(DataPath);
// Operator/node identity store (DeviceIdentity). Lives at %APPDATA%\OpenClawTray
// by convention so it follows the user across machines via roaming profile.
// OPENCLAW_TRAY_APPDATA_DIR isolates a test/E2E identity store the same way
Expand All @@ -288,7 +289,7 @@
if (allowedLocales.Contains(langOverride.ToLowerInvariant()))
LocalizationHelper.SetLanguageOverride(langOverride);
else
Logger.Warn($"[App] Ignoring invalid OPENCLAW_LANGUAGE value: {langOverride}");

Check warning on line 292 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 292 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 292 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 292 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 292 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 292 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.
}

InitializeComponent();
Expand Down Expand Up @@ -325,7 +326,7 @@
MarkRunEnded();
try
{
Logger.Info($"Process exiting (ExitCode={Environment.ExitCode})");

Check warning on line 329 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 329 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 329 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 329 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 329 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 329 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.
}
catch { }
}
Expand All @@ -347,11 +348,11 @@
{
if (ex != null)
{
Logger.Error($"CRASH {source}: {ex}");

Check warning on line 351 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 351 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 351 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 351 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 351 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 351 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.
}
else
{
Logger.Error($"CRASH {source}");

Check warning on line 355 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 355 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 355 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 355 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.
}
}
catch { /* Ignore logging failures */ }
Expand Down Expand Up @@ -514,7 +515,7 @@
if (File.Exists(RunMarkerPath))
{
var startedAt = File.ReadAllText(RunMarkerPath);
Logger.Error($"Previous session did not exit cleanly (started {startedAt})");

Check warning on line 518 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

The type 'Logger' in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'D:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.

Check warning on line 518 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type 'Logger' in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs' conflicts with the imported type 'Logger' in 'OpenClawTray.FunctionalUI, Version=0.5.1.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in 'C:\a\openclaw-windows-node\openclaw-windows-node\src\OpenClaw.Tray.WinUI\Services\Logger.cs'.
File.Delete(RunMarkerPath);
}
}
Expand Down Expand Up @@ -796,7 +797,7 @@
? _startupArgs[1] : null);
if (startupDeepLink != null)
{
HandleDeepLink(startupDeepLink);
await HandleDeepLinkAsync(startupDeepLink);
}

Logger.Info("Application started (WinUI 3)");
Expand Down Expand Up @@ -1249,6 +1250,28 @@
return result == ContentDialogResult.Primary;
}

private async Task<bool> ConfirmDeepLinkActionAsync(DeepLinkResult result)
{
var root = _keepAliveWindow?.Content as FrameworkElement;
if (root?.XamlRoot == null)
{
Logger.Warn($"Cannot confirm deep link action without XAML root: {DeepLinkSecurityPolicy.RedactForLog($"openclaw://{result.Path}")}");
return false;
}

var dialog = new ContentDialog
{
Title = "Confirm OpenClaw action",
Content = $"A deep link wants to {DeepLinkSecurityPolicy.GetActionDisplayName(result)}.",
PrimaryButtonText = "Allow",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Close,
XamlRoot = root.XamlRoot
};
var dialogResult = await dialog.ShowAsync();
return dialogResult == ContentDialogResult.Primary;
}

private void AddRecentActivity(
string line,
string category = "general",
Expand Down Expand Up @@ -3278,21 +3301,41 @@
{
try
{
using var pipe = new NamedPipeServerStream(PipeName, PipeDirection.In);
using var pipe = new NamedPipeServerStream(
DeepLinkPipeName,
PipeDirection.In,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly,
inBufferSize: DeepLinkSecurityPolicy.MaxIpcMessageBytes,
outBufferSize: 0);
await pipe.WaitForConnectionAsync(token);
using var reader = new System.IO.StreamReader(pipe);
var uri = await reader.ReadLineAsync(token);
var uri = await ReadDeepLinkIpcPayloadAsync(pipe, token);
if (!string.IsNullOrEmpty(uri))
{
Logger.Info($"Received deep link via IPC: {uri}");
OnUiThread(() => HandleDeepLink(uri));
Logger.Info($"Received deep link via IPC: {DeepLinkSecurityPolicy.RedactForLog(uri)}");
OnUiThread(() => _ = HandleDeepLinkAsync(uri));
}
}
catch (OperationCanceledException)
{
Logger.Info("Deep link server stopping (canceled)");
break; // Normal shutdown
}
catch (InvalidDataException ex)
{
if (!token.IsCancellationRequested)
{
Logger.Warn($"Rejected deep link IPC payload: {ex.Message}");
}
}
catch (TimeoutException ex)
{
if (!token.IsCancellationRequested)
{
Logger.Warn($"Rejected deep link IPC payload: {ex.Message}");
}
}
catch (Exception ex)
{
if (!token.IsCancellationRequested)
Expand All @@ -3305,6 +3348,77 @@
}, token);
}

private static async Task<string?> ReadDeepLinkIpcPayloadAsync(Stream stream, CancellationToken appToken)
{
using var readCts = CancellationTokenSource.CreateLinkedTokenSource(appToken);
readCts.CancelAfter(DeepLinkSecurityPolicy.IpcReadTimeout);

var scratch = new byte[1024];
var payload = new byte[DeepLinkSecurityPolicy.MaxIpcMessageBytes + 1];
var totalBytes = 0;

try
{
while (true)
{
var remaining = payload.Length - totalBytes;
if (remaining <= 0)
throw new InvalidDataException("payload exceeds maximum size");

var read = await stream.ReadAsync(
scratch.AsMemory(0, Math.Min(scratch.Length, remaining)),
readCts.Token);
if (read == 0)
break;

scratch.AsSpan(0, read).CopyTo(payload.AsSpan(totalBytes));
totalBytes += read;
if (totalBytes > DeepLinkSecurityPolicy.MaxIpcMessageBytes)
throw new InvalidDataException("payload exceeds maximum size");
}
}
catch (OperationCanceledException) when (!appToken.IsCancellationRequested)
{
throw new TimeoutException("timed out while reading payload");
}

if (totalBytes == 0)
return null;

try
{
return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)
.GetString(payload, 0, totalBytes)
.TrimEnd('\r', '\n');
}
catch (DecoderFallbackException ex)
{
throw new InvalidDataException("payload is not valid UTF-8", ex);
}
}

private async Task HandleDeepLinkAsync(string uri)
{
var result = DeepLinkParser.ParseDeepLink(uri);
if (result == null)
{
Logger.Warn($"Rejected invalid deep link: {DeepLinkSecurityPolicy.RedactForLog(uri)}");
return;
}

if (DeepLinkSecurityPolicy.RequiresConfirmation(result))
{
var confirmed = await ConfirmDeepLinkActionAsync(result);
if (!confirmed)
{
Logger.Warn($"Rejected unconfirmed deep link action: {DeepLinkSecurityPolicy.RedactForLog(uri)}");
return;
}
}

HandleDeepLink(uri);
}

private void HandleDeepLink(string uri)
{
DeepLinkHandler.Handle(uri, new DeepLinkActions
Expand Down Expand Up @@ -3363,11 +3477,29 @@
{
try
{
using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
if (!DeepLinkSecurityPolicy.IsIpcPayloadWithinLimit(uri))
{
Logger.Warn($"Rejected oversized deep link before IPC forwarding: {DeepLinkSecurityPolicy.RedactForLog(uri)}");
return;
}

if (DeepLinkParser.ParseDeepLink(uri) == null)
{
Logger.Warn($"Rejected invalid deep link before IPC forwarding: {DeepLinkSecurityPolicy.RedactForLog(uri)}");
return;
}

var payload = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)
.GetBytes(uri);
using var pipe = new NamedPipeClientStream(
".",
DeepLinkPipeName,
PipeDirection.Out,
PipeOptions.CurrentUserOnly);
pipe.Connect(1000);
using var writer = new System.IO.StreamWriter(pipe);
writer.WriteLine(uri);
writer.Flush();
pipe.Write(payload, 0, payload.Length);
pipe.Flush();
pipe.WaitForPipeDrain();
}
catch (Exception ex)
{
Expand Down
4 changes: 2 additions & 2 deletions src/OpenClaw.Tray.WinUI/Services/DeepLinkHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static void Handle(string uri, DeepLinkActions actions)

var path = result.Path?.TrimEnd('/') ?? string.Empty;

Logger.Info($"Handling deep link: {path}");
Logger.Info($"Handling deep link: {DeepLinkSecurityPolicy.RedactForLog(uri)}");

switch (path.ToLowerInvariant())
{
Expand Down Expand Up @@ -239,7 +239,7 @@ public static void Handle(string uri, DeepLinkActions actions)
try
{
await actions.SendMessage(agentMessage);
Logger.Info($"Sent message via deep link: {agentMessage}");
Logger.Info("Sent message via deep link");
}
catch (Exception ex)
{
Expand Down
119 changes: 119 additions & 0 deletions src/OpenClaw.Tray.WinUI/Services/DeepLinkSecurityPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using OpenClaw.Shared;
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;

namespace OpenClawTray.Services;

internal static class DeepLinkSecurityPolicy
{
public const int MaxIpcMessageBytes = 8192;
public static readonly TimeSpan IpcReadTimeout = TimeSpan.FromSeconds(2);

private const string PipeNamePrefix = "OpenClawTray-DeepLink";

private static readonly HashSet<string> StateChangingPaths = new(StringComparer.OrdinalIgnoreCase)
{
"send",
"agent",
"voice",
"voice-start",
"voice-stop",
"ssh-restart",
"restart-ssh",
"restart-ssh-tunnel"
};

public static string BuildCurrentUserScopedPipeName(string dataPath)
=> BuildPipeName(dataPath, GetCurrentUserScope(), GetCurrentSessionId());

internal static string BuildPipeName(string dataPath, string userScope, int sessionId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(dataPath);
ArgumentException.ThrowIfNullOrWhiteSpace(userScope);

var scope = $"{userScope}|{sessionId}|{Path.GetFullPath(dataPath)}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(scope));
return $"{PipeNamePrefix}-{Convert.ToHexString(hash, 0, 8)}";
}

public static bool IsIpcPayloadWithinLimit(string? uri)
=> !string.IsNullOrEmpty(uri) && Encoding.UTF8.GetByteCount(uri) <= MaxIpcMessageBytes;

public static bool IsStateChangingPath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return false;

var normalized = path.Trim().Trim('/').ToLowerInvariant();
if (StateChangingPaths.Contains(normalized))
return true;

var slashIndex = normalized.IndexOf('/');
return slashIndex > 0 && StateChangingPaths.Contains(normalized[..slashIndex]);
}

public static bool RequiresConfirmation(DeepLinkResult? result)
=> result != null && IsStateChangingPath(result.Path);

public static string RedactForLog(string? uri)
{
if (string.IsNullOrWhiteSpace(uri))
return "<empty-deep-link>";

var result = DeepLinkParser.ParseDeepLink(uri);
if (result == null)
return "<invalid-deep-link>";

var redactedPath = RedactPathForLog(result.Path);
return string.IsNullOrEmpty(result.Query)
? $"openclaw://{redactedPath}"
: $"openclaw://{redactedPath}?<redacted>";
}

internal static string GetActionDisplayName(DeepLinkResult result)
{
var path = result.Path.Trim().Trim('/').ToLowerInvariant();
return path switch
{
"send" => "open the quick-send window with a prefilled message",
"agent" => "send a message to the agent",
"voice" or "voice-start" => "start voice input",
"voice-stop" => "stop voice input",
"ssh-restart" or "restart-ssh" or "restart-ssh-tunnel" => "restart the SSH tunnel",
_ => "run this OpenClaw action"
};
}

internal static string RedactPathForLog(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return "";

var normalized = path.Trim().Trim('/');
if (normalized.Length == 0)
return "";

var slashIndex = normalized.IndexOf('/');
var firstSegment = slashIndex >= 0 ? normalized[..slashIndex] : normalized;

return slashIndex >= 0 ? $"{firstSegment}/..." : firstSegment;
}

private static string GetCurrentUserScope()
{
if (OperatingSystem.IsWindows())
{
var sid = WindowsIdentity.GetCurrent().User?.Value;
if (!string.IsNullOrWhiteSpace(sid))
return sid;
}

return $"{Environment.MachineName}\\{Environment.UserName}";
}

private static int GetCurrentSessionId()
=> OperatingSystem.IsWindows() ? Process.GetCurrentProcess().SessionId : 0;
}
Loading
Loading