Skip to content
Open
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
20 changes: 20 additions & 0 deletions src/OpenClaw.Connection/ConnectionStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal sealed class ConnectionStateMachine
private RoleConnectionState _nodeState = RoleConnectionState.Idle;
private string? _operatorError;
private string? _nodeError;
private string? _operatorCredentialSource;
private string? _nodeCredentialSource;
private bool _nodeEnabled;

/// <summary>
Expand Down Expand Up @@ -134,6 +136,8 @@ public void Reset()
_nodeState = _nodeEnabled ? RoleConnectionState.Idle : RoleConnectionState.Disabled;
_operatorError = null;
_nodeError = null;
_operatorCredentialSource = null;
_nodeCredentialSource = null;
RebuildSnapshot();
}

Expand All @@ -154,6 +158,12 @@ internal void SetOperatorDeviceId(string? deviceId)
Current = Current with { OperatorDeviceId = deviceId };
}

internal void SetOperatorCredentialSource(string? source)
{
_operatorCredentialSource = source;
RebuildSnapshot();
}

/// <summary>Update node info (device ID, pairing status, optional request ID) in the snapshot.</summary>
internal void SetNodeInfo(string? deviceId, OpenClaw.Shared.PairingStatus pairingStatus, string? pairingRequestId = null)
{
Expand All @@ -165,6 +175,12 @@ internal void SetNodeInfo(string? deviceId, OpenClaw.Shared.PairingStatus pairin
};
}

internal void SetNodeCredentialSource(string? source)
{
_nodeCredentialSource = source;
RebuildSnapshot();
}

/// <summary>Update the operator pairing request ID in the snapshot.</summary>
internal void SetOperatorPairingRequestId(string? requestId)
{
Expand Down Expand Up @@ -239,6 +255,8 @@ private void ApplyTransition(ConnectionTrigger trigger, string? detail)
_nodeState = _nodeEnabled ? RoleConnectionState.Idle : RoleConnectionState.Disabled;
_operatorError = null;
_nodeError = null;
_operatorCredentialSource = null;
_nodeCredentialSource = null;
break;

case ConnectionTrigger.ReconnectScheduled:
Expand Down Expand Up @@ -299,12 +317,14 @@ private void RebuildSnapshot()
OverallState = GatewayConnectionSnapshot.DeriveOverall(_operatorState, _nodeState, _nodeEnabled),
OperatorState = _operatorState,
OperatorError = _operatorError,
OperatorCredentialSource = _operatorCredentialSource,
OperatorPairingRequired = _operatorState == RoleConnectionState.PairingRequired,
// Clear requestId when no longer in PairingRequired to prevent stale reads
OperatorPairingRequestId = _operatorState == RoleConnectionState.PairingRequired
? Current.OperatorPairingRequestId : null,
NodeState = _nodeState,
NodeError = _nodeError,
NodeCredentialSource = _nodeCredentialSource,
// Clear requestId when no longer in PairingRequired to prevent stale reads
NodePairingRequestId = _nodeState == RoleConnectionState.PairingRequired
? Current.NodePairingRequestId : null,
Expand Down
5 changes: 5 additions & 0 deletions src/OpenClaw.Connection/GatewayConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ private async Task ConnectCoreAsync(string? gatewayId = null)
var prev = _stateMachine.Current.OverallState;
// Must go through Connecting → Error since AuthenticationFailed requires Connecting state
_stateMachine.TryTransition(ConnectionTrigger.ConnectRequested);
_stateMachine.SetOperatorCredentialSource(null);
_stateMachine.TryTransition(ConnectionTrigger.AuthenticationFailed, "No credential available");
EmitStateChanged(prev);
return;
Expand All @@ -190,6 +191,7 @@ private async Task ConnectCoreAsync(string? gatewayId = null)
// Transition to Connecting
var prevState = _stateMachine.Current.OverallState;
_stateMachine.TryTransition(ConnectionTrigger.ConnectRequested);
_stateMachine.SetOperatorCredentialSource(credential.Source);
_diagnostics.RecordStateChange(prevState, _stateMachine.Current.OverallState);
EmitStateChanged(prevState);

Expand Down Expand Up @@ -347,6 +349,8 @@ private async Task<bool> PrepareNodeOnlyConnectCoreAsync(string? gatewayId = nul
};

_diagnostics.RecordCredentialResolution(nodeCredential);
_stateMachine.SetOperatorCredentialSource(null);
_stateMachine.SetNodeCredentialSource(nodeCredential.Source);
_diagnostics.Record("node", $"Starting node-only connection to {record.Url}",
$"Credential source: {nodeCredential.Source}");

Expand Down Expand Up @@ -931,6 +935,7 @@ private async Task<bool> StartNodeConnectionAsync()
try
{
_stateMachine.SetNodeEnabled(true);
_stateMachine.SetNodeCredentialSource(nodeCredential.Source);
}
finally
{
Expand Down
2 changes: 2 additions & 0 deletions src/OpenClaw.Connection/GatewayConnectionSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed record GatewayConnectionSnapshot
public string? OperatorError { get; init; }
public bool OperatorPairingRequired { get; init; }
public string? OperatorDeviceId { get; init; }
public string? OperatorCredentialSource { get; init; }
/// <summary>
/// The requestId returned by the gateway when operator pairing is required.
/// Used by setup flows to approve the specific pairing request via CLI.
Expand All @@ -25,6 +26,7 @@ public sealed record GatewayConnectionSnapshot
public string? NodeError { get; init; }
public OpenClaw.Shared.PairingStatus NodePairingStatus { get; init; }
public string? NodeDeviceId { get; init; }
public string? NodeCredentialSource { get; init; }
/// <summary>
/// The requestId returned by the gateway when node pairing is required.
/// Used by the connection page to show the correct approval command.
Expand Down
23 changes: 20 additions & 3 deletions src/OpenClaw.SetupEngine.UI/Pages/CompletePage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace OpenClaw.SetupEngine.UI.Pages;

public sealed partial class CompletePage : Page
{
private const string StartupRunKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string StartupRunValue = "OpenClawTray";
private static readonly Regex s_urlRegex = new(@"https?://[^\s)]+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private string? _logPath;

Expand Down Expand Up @@ -144,9 +146,24 @@ private static void RegisterStartup()
var trayPath = TrayExecutableResolver.Resolve();
if (trayPath == null) return;

using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", writable: true);
key?.SetValue("OpenClawTray", $"\"{Path.GetFullPath(trayPath)}\"");
if (StartupTaskRegistration.Register(trayPath))
{
DeleteLegacyStartupRunValue();
return;
}

using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(StartupRunKey, writable: true);
key?.SetValue(StartupRunValue, $"\"{Path.GetFullPath(trayPath)}\"");
}
catch { /* best effort */ }
}

private static void DeleteLegacyStartupRunValue()
{
try
{
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(StartupRunKey, writable: true);
key?.DeleteValue(StartupRunValue, throwOnMissingValue: false);
}
catch { /* best effort */ }
}
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.SetupEngine/OpenClaw.SetupEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<ProjectReference Include="..\OpenClaw.Connection\OpenClaw.Connection.csproj" />
<ProjectReference Include="..\OpenClaw.Shared\OpenClaw.Shared.csproj" />
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions src/OpenClaw.SetupEngine/StartupTaskRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Diagnostics;
using OpenClaw.Shared;

namespace OpenClaw.SetupEngine;

public static class StartupTaskRegistration
{
internal const string TaskName = WindowsStartupTaskRegistration.TaskName;

public static bool Register(string trayExecutablePath) =>
WindowsStartupTaskRegistration.Register(trayExecutablePath);

public static bool Unregister() =>
WindowsStartupTaskRegistration.Unregister();

internal static ProcessStartInfo CreateRegisterProcessStartInfo(string trayExecutablePath) =>
WindowsStartupTaskRegistration.CreateRegisterProcessStartInfo(trayExecutablePath);

internal static ProcessStartInfo CreateUnregisterProcessStartInfo() =>
WindowsStartupTaskRegistration.CreateUnregisterProcessStartInfo();

internal static ProcessStartInfo CreateQueryProcessStartInfo() =>
WindowsStartupTaskRegistration.CreateQueryProcessStartInfo();

internal static string ResolveSchtasksPath() =>
WindowsStartupTaskRegistration.ResolveSchtasksPath();
}
8 changes: 7 additions & 1 deletion src/OpenClaw.SetupEngine/TrayArtifactCleanup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Runtime.Versioning;
using Microsoft.Win32;
using OpenClaw.Connection;
using OpenClaw.Shared;

namespace OpenClaw.SetupEngine;

Expand All @@ -20,7 +21,7 @@ public static void Run(SetupContext ctx, bool preserveLogs = false)
var appDataDir = ctx.DataDir; // %APPDATA%\OpenClawTray
var localDataDir = ctx.LocalDataDir;

// 1. Remove autostart registry key
// 1. Remove autostart entries
try
{
using var key = Registry.CurrentUser.OpenSubKey(AutoStartKey, writable: true);
Expand All @@ -39,6 +40,11 @@ public static void Run(SetupContext ctx, bool preserveLogs = false)
logger.Warn($"[Uninstall] Failed to remove autostart registry key: {ex.Message}");
}

if (WindowsStartupTaskRegistration.Unregister())
logger.Info("[Uninstall] Removed autostart scheduled task");
else
logger.Info("[Uninstall] Autostart scheduled task already absent or unavailable");

// 2. Delete run.marker
DeleteFileIfExists(Path.Combine(localDataDir, "run.marker"), "run.marker", logger);

Expand Down
17 changes: 15 additions & 2 deletions src/OpenClaw.Shared/InstanceMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ private static MergedInstance BuildFromPresence(
IsThisInstance = !isGateway && IsLocalIdentity(node, p, options),
DisplayName = node?.DisplayName is { Length: > 0 } dn ? dn : p.DisplayName,
Ip = p.Ip ?? node?.RemoteIp,
Version = p.Version ?? node?.Version,
Version = p.Version ?? DisplayVersionForNode(node, hasPresence: true),
Platform = p.Platform ?? node?.Platform,
DeviceFamily = p.DeviceFamily ?? node?.DeviceFamily,
ModelIdentifier = p.ModelIdentifier ?? node?.ModelIdentifier,
Expand Down Expand Up @@ -279,7 +279,7 @@ private static MergedInstance BuildFromOrphanNode(
IsThisInstance = IsLocalIdentity(node, presence: null, options),
DisplayName = string.IsNullOrWhiteSpace(node.DisplayName) ? node.ShortId : node.DisplayName,
Ip = node.RemoteIp,
Version = node.Version,
Version = DisplayVersionForNode(node, hasPresence: false),
Platform = node.Platform,
DeviceFamily = node.DeviceFamily,
ModelIdentifier = node.ModelIdentifier,
Expand Down Expand Up @@ -310,6 +310,19 @@ private static MergedInstance BuildFromOrphanNode(
return n;
}

private static string? DisplayVersionForNode(GatewayNodeInfo? node, bool hasPresence)
{
var version = node?.Version;
if (!hasPresence &&
node is { IsOnline: false } &&
string.Equals(version?.Trim(), "1.0.0", StringComparison.OrdinalIgnoreCase))
{
return null;
}

return version;
}

private static PresenceStatus ClassifyPresence(
PresenceEntry p,
DateTime nowUtc,
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Shared/OpenClaw.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="OpenClaw.SetupEngine" />
<InternalsVisibleTo Include="OpenClaw.Shared.Tests" />
</ItemGroup>

Expand All @@ -24,4 +25,3 @@

</Project>


89 changes: 89 additions & 0 deletions src/OpenClaw.Shared/WindowsStartupTaskRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Diagnostics;

namespace OpenClaw.Shared;

public static class WindowsStartupTaskRegistration
{
public const string TaskName = "OpenClaw Companion";

public static bool Register(string trayExecutablePath)
{
if (string.IsNullOrWhiteSpace(trayExecutablePath) || !File.Exists(trayExecutablePath))
return false;

return Run(CreateRegisterProcessStartInfo(trayExecutablePath));
}

public static bool Unregister() => Run(CreateUnregisterProcessStartInfo());

public static bool Exists() => Run(CreateQueryProcessStartInfo());

internal static ProcessStartInfo CreateRegisterProcessStartInfo(string trayExecutablePath)
{
var fullPath = Path.GetFullPath(trayExecutablePath);
return CreateStartInfo(
"/Create",
"/TN", TaskName,
"/TR", Quote(fullPath),
"/SC", "ONLOGON",
"/F");
}

internal static ProcessStartInfo CreateUnregisterProcessStartInfo() =>
CreateStartInfo(
"/Delete",
"/TN", TaskName,
"/F");

internal static ProcessStartInfo CreateQueryProcessStartInfo() =>
CreateStartInfo(
"/Query",
"/TN", TaskName);

private static bool Run(ProcessStartInfo startInfo)
{
try
{
using var process = Process.Start(startInfo);
if (process == null)
return false;

process.WaitForExit(10_000);
return process.HasExited && process.ExitCode == 0;
}
catch
{
return false;
}
}

internal static string ResolveSchtasksPath()
{
var systemRoot = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
if (string.IsNullOrWhiteSpace(systemRoot))
systemRoot = Environment.GetEnvironmentVariable("SystemRoot");

return !string.IsNullOrWhiteSpace(systemRoot)
? Path.Combine(systemRoot, "System32", "schtasks.exe")
: Path.Combine("C:\\", "Windows", "System32", "schtasks.exe");
}

private static ProcessStartInfo CreateStartInfo(params string[] arguments)
{
var startInfo = new ProcessStartInfo
{
FileName = ResolveSchtasksPath(),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};

foreach (var argument in arguments)
startInfo.ArgumentList.Add(argument);

return startInfo;
}

private static string Quote(string value) => "\"" + value.Replace("\"", "\\\"") + "\"";
}
Loading
Loading