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
6 changes: 6 additions & 0 deletions docs/CONNECTION_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,12 @@ On first startup with a `GatewayRegistry`, if no active gateway record exists, t
- `LegacyBootstrapToken` → `GatewayRecord.BootstrapToken`
- Old identity file copied into per-gateway identity directory

Legacy credential and identity migration requires a non-empty `GatewayUrl`
persisted in `settings.json`. The in-memory default loopback URL is not a saved
gateway target and never receives a URL-less legacy token or root identity.
Profiles left in that state by an older uninstaller fail closed and must be
reconnected explicitly from the Connection page.

Migration is idempotent and deduplicates by URL.

## Signature protocol
Expand Down
13 changes: 13 additions & 0 deletions docs/SETUP_ENGINE_REDESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,19 @@ Structured JSONL logger. Records sanitized entries for:

Log path defaults to `%APPDATA%\OpenClawTray\Logs\Setup\setup-engine-<yyyyMMdd-HHmmss>.jsonl` for setup and `uninstall-engine-<yyyyMMdd-HHmmss>.jsonl` for uninstall.

### Uninstall onboarding reset

The Inno uninstaller invokes `scripts\Uninstall-LocalGateway.ps1` directly,
not the C# uninstall engine. Both onboarding reset paths remove `GatewayUrl`
and legacy `Token` / `BootstrapToken` properties from `settings.json`, even
when node mode and autostart settings are preserved for remaining gateways.
Other preferences and external gateway records are preserved. The focused
`UninstallOnboardingSettingsTests` execute only the production PowerShell
reset and its JSON/logging helpers under Windows PowerShell 5.1 with temporary
files; they do not prove the full signed-installer uninstall path.
On a later launch, a URL-less legacy root identity is not migrated or used
against the default loopback URL. The profile must be reconnected explicitly.

---

## UI Flow
Expand Down
10 changes: 6 additions & 4 deletions scripts/Uninstall-LocalGateway.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -568,9 +568,11 @@ function Resolve-AppDataDir {
}

$changed = $false
if ($settings.PSObject.Properties['GatewayUrl']) {
$settings.PSObject.Properties.Remove('GatewayUrl')
$changed = $true
foreach ($propertyName in @('GatewayUrl', 'Token', 'BootstrapToken')) {
if ($settings.PSObject.Properties[$propertyName]) {
$settings.PSObject.Properties.Remove($propertyName)
$changed = $true
}
}

if (-not $PreserveNodeSettings -and $settings.PSObject.Properties['EnableNodeMode']) {
Expand All @@ -590,7 +592,7 @@ function Resolve-AppDataDir {

try {
Write-JsonFileAtomic -Path $settingsPath -Value $settings
Write-GatewayLog 'Reset onboarding settings.'
Write-GatewayLog 'Reset onboarding settings; legacy gateway fields are absent.'
} catch {
Add-CleanupWarning "Failed to reset onboarding settings: $($_.Exception.Message)"
}
Expand Down
13 changes: 6 additions & 7 deletions scripts/validate-wsl-gateway-uninstall.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -857,23 +857,22 @@ function Invoke-UninstallSteps {
try {
if ($IsDryRun) {
Add-Step -Name 'reset-onboarding-settings' -Status 'DryRun' `
-Message "Would reset Token='', BootstrapToken='', GatewayUrl='ws://localhost:18789'. EnableMcpServer preserved. Tokens: ***REDACTED***"
-Message 'Would remove Token, BootstrapToken, and GatewayUrl. EnableMcpServer preserved. Tokens: ***REDACTED***'
} elseif (-not (Test-Path -LiteralPath $settingsPath)) {
Add-Step -Name 'reset-onboarding-settings' -Status 'Skipped' `
-Message 'settings.json not found.'
} else {
$sRaw = Get-Content -LiteralPath $settingsPath -Raw -Encoding UTF8
$sObj = $sRaw | ConvertFrom-Json
foreach ($field in @('Token', 'BootstrapToken')) {
if ($sObj.PSObject.Properties[$field]) { $sObj.$field = '' }
}
if ($sObj.PSObject.Properties['GatewayUrl']) {
$sObj.GatewayUrl = 'ws://localhost:18789'
foreach ($field in @('Token', 'BootstrapToken', 'GatewayUrl')) {
if ($sObj.PSObject.Properties[$field]) {
$sObj.PSObject.Properties.Remove($field)
}
}
# EnableMcpServer: NOT touched.
$sObj | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $settingsPath -Encoding UTF8
Add-Step -Name 'reset-onboarding-settings' -Status 'Executed' `
-Message 'Token=***REDACTED***, BootstrapToken=***REDACTED***, GatewayUrl reset. EnableMcpServer preserved.'
-Message 'Token, BootstrapToken, and GatewayUrl removed. EnableMcpServer preserved.'
}
} catch {
$stepErrors.Add("reset-onboarding-settings: $($_.Exception.Message)")
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private static (string GatewayUrl, string Token, SettingsData? Loaded) LoadConne

if (!settings.UseSshTunnel)
{
return settings.GatewayUrl;
return settings.GatewayUrl ?? "ws://127.0.0.1:18789";
}

var port = settings.SshTunnelLocalPort <= 0 ? 18789 : settings.SshTunnelLocalPort;
Expand Down
18 changes: 11 additions & 7 deletions src/OpenClaw.SetupEngine/TrayArtifactCleanup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,16 @@ internal static void ResetOnboardingSettings(string appDataDir, SetupLogger logg

bool changed = false;

// Reset GatewayUrl to empty
if (dict.ContainsKey("GatewayUrl"))
{
dict.Remove("GatewayUrl");
// Drop the saved URL and legacy Token / BootstrapToken together, even
// when preserveNodeSettings is true. A later launch falls back to the
// default local URL and must not import leftovers. Node mode and
// autostart stay only when that flag is set.
if (dict.Remove("GatewayUrl"))
changed = true;
if (dict.Remove("Token"))
changed = true;
if (dict.Remove("BootstrapToken"))
changed = true;
}

if (!preserveNodeSettings && dict.ContainsKey("EnableNodeMode"))
{
Expand All @@ -187,8 +191,8 @@ internal static void ResetOnboardingSettings(string appDataDir, SetupLogger logg
var updatedJson = System.Text.Json.JsonSerializer.Serialize(dict, SetupConfig.JsonWriteOptions);
AtomicFile.WriteAllText(settingsPath, updatedJson);
logger.Info(preserveNodeSettings
? "[Uninstall] Reset onboarding settings (GatewayUrl)"
: "[Uninstall] Reset onboarding settings (GatewayUrl, EnableNodeMode, AutoStart)");
? "[Uninstall] Reset onboarding settings (GatewayUrl, Token, BootstrapToken); node settings preserved"
: "[Uninstall] Reset onboarding settings (GatewayUrl, Token, BootstrapToken, EnableNodeMode, AutoStart)");
}
else
{
Expand Down
10 changes: 5 additions & 5 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1840,7 +1840,7 @@ private void InitializeGatewayClient(bool useBootstrapHandoffAuth = false)
if (_settings == null || _connectionManager == null || _gatewayRegistry == null) return;
// SSH tunnel lifecycle is now handled by the connection manager.

var gatewayUrl = _settings.GetEffectiveGatewayUrl();
var gatewayUrl = _settings.GetLegacyCredentialGatewayUrlOrNull();

// Check registry first — it's the source of truth after initial setup
var activeRecord = _gatewayRegistry.GetActive();
Expand Down Expand Up @@ -2115,7 +2115,7 @@ private async Task<bool> ResendOpenTelemetryProbeAsync()

// Backfill for legacy installs that still have the identity file at the
// root settings path while the active registry record points at that URL.
var effectiveUrl = _settings?.GetEffectiveGatewayUrl();
var effectiveUrl = _settings?.GetLegacyCredentialGatewayUrlOrNull();
if (!string.IsNullOrWhiteSpace(effectiveUrl) &&
string.Equals(record.Url, effectiveUrl, StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -2136,7 +2136,7 @@ private async Task<bool> ResendOpenTelemetryProbeAsync()
if (credential != null)
return credential;

var effectiveUrl = _settings?.GetEffectiveGatewayUrl();
var effectiveUrl = _settings?.GetLegacyCredentialGatewayUrlOrNull();
if (string.IsNullOrWhiteSpace(effectiveUrl) ||
!string.Equals(record.Url, effectiveUrl, StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -2192,7 +2192,7 @@ private static void TryCopyLegacyIdentityToGateway(string gatewayId, string iden
}
}

private void TryMigrateLegacyGatewaySettings(string gatewayUrl, IOpenClawLogger logger)
private void TryMigrateLegacyGatewaySettings(string? gatewayUrl, IOpenClawLogger logger)
{
if (_settings == null || _gatewayRegistry == null || string.IsNullOrWhiteSpace(gatewayUrl))
{
Expand Down Expand Up @@ -3971,7 +3971,7 @@ private bool TryResolveChatCredentials(
_gatewayRegistry,
SettingsManager.SettingsDirectoryPath,
DeviceIdentityFileReader.Instance,
_settings.GetEffectiveGatewayUrl(),
_settings.GetLegacyCredentialGatewayUrlOrNull(),
_settings.LegacyToken,
_settings.LegacyBootstrapToken,
(record, candidate) =>
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Tray.WinUI/Pages/ChatPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ private async Task ApplyWebViewSurfaceAsync(SettingsManager settings, int genera
// Capture on the UI thread (continuation resumes here); resolve off it.
var registry = CurrentApp.Registry;
var authorizer = CurrentApp.InteractiveEndpointAuthorizer;
var gatewayUrl = settings.GetEffectiveGatewayUrl();
var gatewayUrl = settings.GetLegacyCredentialGatewayUrlOrNull();
var legacyToken = settings.LegacyToken;
var legacyBootstrapToken = settings.LegacyBootstrapToken;
credential = await Task.Run(() => ResolveChatCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,9 @@ private sealed record ConnectionSettingsSnapshot(
{
public static ConnectionSettingsSnapshot Capture(SettingsManager settings) =>
new(
settings.GatewayUrl,
// The getter substitutes the setup default. Snapshot only a URL
// the user saved, so rollback cannot persist that default.
settings.PersistedGatewayUrl ?? "",
settings.UseSshTunnel,
settings.SshTunnelUser,
settings.SshTunnelHost,
Expand Down
59 changes: 54 additions & 5 deletions src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ public class SettingsManager
private SettingsData _data = CreateDefaultData();

// Connection
public string GatewayUrl { get => _data.GatewayUrl ?? AppIdentity.SetupGatewayUrl; set => _data = _data with { GatewayUrl = value }; }
public string GatewayUrl
{
get => _data.GatewayUrl ?? AppIdentity.SetupGatewayUrl;
set
{
_data = _data with { GatewayUrl = value };
HasPersistedGatewayUrl = !string.IsNullOrWhiteSpace(value);
}
}
public bool UseSshTunnel { get => _data.UseSshTunnel; set => _data = _data with { UseSshTunnel = value }; }
public string SshTunnelUser { get => _data.SshTunnelUser ?? ""; set => _data = _data with { SshTunnelUser = value }; }
public string SshTunnelHost { get => _data.SshTunnelHost ?? ""; set => _data = _data with { SshTunnelHost = value }; }
Expand All @@ -54,6 +62,22 @@ public class SettingsManager
public int? BrowserControlPort { get => _data.BrowserControlPort; set => _data = _data with { BrowserControlPort = value }; }
public string? LegacyToken { get; private set; }
public string? LegacyBootstrapToken { get; private set; }
/// <summary>
/// True when settings.json stored a non-empty GatewayUrl. The in-memory
/// default URL is not a saved gateway and must not receive legacy secrets.
/// </summary>
public bool HasPersistedGatewayUrl { get; private set; }

/// <summary>
/// Gateway URL last saved by the user, or null when none was saved.
/// <see cref="GatewayUrl"/> fills in the setup default, so rollback must
/// copy this value instead of the getter.
/// </summary>
internal string? PersistedGatewayUrl =>
HasPersistedGatewayUrl && !string.IsNullOrWhiteSpace(_data.GatewayUrl)
? _data.GatewayUrl
: null;

public bool HasLegacyGatewayCredentials =>
!string.IsNullOrWhiteSpace(LegacyToken) ||
!string.IsNullOrWhiteSpace(LegacyBootstrapToken);
Expand Down Expand Up @@ -229,6 +253,7 @@ public void Load()
_persistedJson = null;
LegacyToken = null;
LegacyBootstrapToken = null;
HasPersistedGatewayUrl = false;
_data = CreateDefaultData();

try
Expand All @@ -251,6 +276,7 @@ public void Load()
Logger.Warn($"Failed to load settings: {ex.Message}");
LegacyToken = null;
LegacyBootstrapToken = null;
HasPersistedGatewayUrl = false;
}
if (loadSucceeded && _hasPersistenceConflict)
{
Expand Down Expand Up @@ -345,7 +371,7 @@ private static SettingsData NormalizeLoadedData(SettingsData loaded, string? raw
var data = loaded with
{
SettingsSchemaVersion = CurrentSettingsSchemaVersion,
GatewayUrl = loaded.GatewayUrl ?? defaults.GatewayUrl,
GatewayUrl = loaded.GatewayUrl,
SshTunnelUser = loaded.SshTunnelUser ?? defaults.SshTunnelUser,
SshTunnelHost = loaded.SshTunnelHost ?? defaults.SshTunnelHost,
SshTunnelSshPort = IsValidPort(loaded.SshTunnelSshPort) ? loaded.SshTunnelSshPort : defaults.SshTunnelSshPort,
Expand Down Expand Up @@ -412,10 +438,16 @@ private void LoadLegacyGatewayCredentials(string json)
{
LegacyToken = null;
LegacyBootstrapToken = null;
HasPersistedGatewayUrl = false;

try
{
using var document = JsonDocument.Parse(json);
HasPersistedGatewayUrl = !string.IsNullOrWhiteSpace(
ReadLegacyString(document.RootElement, "GatewayUrl"));
if (!HasPersistedGatewayUrl)
return;

LegacyToken = ReadLegacyString(document.RootElement, "Token");
LegacyBootstrapToken = ReadLegacyString(document.RootElement, "BootstrapToken");
}
Expand All @@ -440,7 +472,7 @@ private void LoadLegacyGatewayCredentials(string json)
/// </summary>
public SettingsData ToSettingsData() => _data with
{
GatewayUrl = GatewayUrl,
GatewayUrl = HasPersistedGatewayUrl ? _data.GatewayUrl : null,
SshTunnelUser = SshTunnelUser,
SshTunnelHost = SshTunnelHost,
SshTunnelRemotePort = SshTunnelRemotePort,
Expand Down Expand Up @@ -608,8 +640,22 @@ internal void UpdateAndSave(Action edit)
lock (_saveLock)
{
var before = ToSettingsData();
try { edit(); SaveOrThrow(); }
catch { _data = before; throw; }
var hadPersistedGatewayUrl = HasPersistedGatewayUrl;
try
{
edit();
SaveOrThrow();
}
catch
{
// The GatewayUrl setter stores explicit-target provenance beside
// SettingsData. Restoring the record alone leaves that flag set,
// and the getter then presents the setup default as a saved gateway.
_data = before;
HasPersistedGatewayUrl = hadPersistedGatewayUrl &&
!string.IsNullOrWhiteSpace(before.GatewayUrl);
throw;
}
}
}

Expand Down Expand Up @@ -698,4 +744,7 @@ public string GetEffectiveGatewayUrl()

return $"ws://127.0.0.1:{SshTunnelLocalPort}";
}

public string? GetLegacyCredentialGatewayUrlOrNull() =>
HasPersistedGatewayUrl ? GetEffectiveGatewayUrl() : null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3397,7 +3397,9 @@ public async Task ConnectNodeOnlyAsync_StalledRetirementDoesNotBlockManagerDisco
await nodeConnector.DisconnectStarted.Task.WaitAsync(TimeSpan.FromSeconds(2));
var disconnect = manager.DisconnectAsync();

await nodeStart.WaitAsync(TimeSpan.FromSeconds(3));
// The coordinator gives the previous disconnect 2 seconds. This wait
// has to outlive that budget when the runner is busy.
await nodeStart.WaitAsync(TimeSpan.FromSeconds(10));
nodeConnector.AllowDisconnect.SetResult(true);
await disconnect.WaitAsync(TimeSpan.FromSeconds(2));

Expand Down
Loading
Loading