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
2 changes: 2 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
| Hub navigation tags, page mapping, command catalog/search, and gateway-page classification | `HubPageRegistry` | authoritative |
| Hub notification banner severity and action projection | `AppNotificationInfoBarPresenter` | authoritative |
| Tray-menu semantic composition and connection-toggle state | `TrayMenuPresenter` + `ConnectionTogglePresenter` | authoritative |
| Dashboard link Tailscale revalidation, browser-credential fallback, and fail-closed URL policy | `GatewayDashboardLinkService` | authoritative |
| App-owned non-tray window creation, reuse, focus, theme, and lifetime | `IWindowManager` + `WindowManager` | authoritative |
| Tray icon, popup coordination, live status, and callback lifetime | `ITrayController` + `TrayController` | authoritative |
| Deep-link/protocol/toast/forwarded activation normalization, current-user IPC, and semantic activation plans | `ActivationRouter` | authoritative |
Expand Down Expand Up @@ -169,6 +170,7 @@ leading and trailing pipe. Columns, in order:
| app-window-surface-ownership-closed | closed | src/OpenClaw.Tray.WinUI/App.xaml.cs | concrete non-tray window fields, constructors, show/hide/focus/theme/close mechanics, and window event lifetime | IWindowManager + WindowManager | interface forwarding, immutable request construction, route and policy callbacks, and setup restart dialog policy only | App cannot regain a parallel Hub, Chat, status, setup, canvas-request, or runtime-anchor owner | AppSurfaceOwnershipContractTests.App_DelegatesConcreteTrayAndWindowOwnership | source-shape | when App is replaced as the WinUI composition root |
| app-tray-controller | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | tray icon creation, tray popup coordination, click routing, tooltip and live-toggle refresh, theme, callbacks, and disposal | ITrayController + TrayController | App captures immutable snapshots, implements semantic action callbacks, triggers refresh from authoritative state, preserves startup construction order, and constructs shutdown-plan callbacks | one tray icon and root menu are reused; A1 presenters retain semantics; TrayMenuWindow retains native popup mechanics; callbacks detach and resources dispose once | TrayControllerTests.Dispose_UnsubscribesAndDisposesEachResourceOnce | source-shape | when the WinUI tray surface is replaced |
| app-tray-surface-ownership-closed | closed | src/OpenClaw.Tray.WinUI/App.xaml.cs | concrete tray icon, root menu, weak live-control state, event subscriptions, popup build coordination, and resource disposal | ITrayController + TrayController | immutable snapshot and action callbacks plus state-change triggers only | App cannot regain tray controls or popup lifetime and TrayController cannot duplicate A1 semantic projection or TrayMenuWindow native mechanics | AppSurfaceOwnershipContractTests.App_DelegatesConcreteTrayAndWindowOwnership | source-shape | when the WinUI tray surface is replaced |
| dashboard-link-policy | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs + App.CapabilityHandlers.cs + Pages/ConnectionPage.xaml.cs | duplicated Tailscale revalidation, browser-credential fallback, and dashboard URL policy | GatewayDashboardLinkService | callers retain UI launch, error, provenance, and MCP response side effects | trusted Tailscale links omit shared credentials; degraded Tailscale revalidation falls back only to an approved shared browser credential and otherwise fails closed; non-Tailscale QR/bootstrap requests preserve the existing token-free dashboard URL | AppRefactorContractTests.DashboardLinkPolicy_StaysDelegatedToFocusedService | source-shape | when App, ConnectionPage, and local MCP no longer own dashboard-link entrypoints |
| native-tool-projector | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | pure native tool identity, allowlisted display arguments, payload extraction, and flattened-history detection/classification/summary | NativeToolProjector | ChatEventMapper and ChatHistoryLoader call the projector; ChatConversationState supplies scoped correlation plans and ChatMetadataStore owns persistence | unknown identities remain truthful Tool; title aliases are strict; display arguments are allowlisted, redacted, and bounded; live/history projection stays consistent | NativeToolProjectorTests.ExtractToolIdentity_TitleRequiresExactTrustedAlias | behavioral | - |
| provider-native-tool-projection-closed | closed | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | private static copies of native tool identity, display argument, payload, flattened-history projection, and scoped metadata upsert | NativeToolProjector + ChatEventMapper + ChatHistoryLoader + ChatConversationState + ChatMetadataStore | provider forwards typed tool metadata writes while retaining bridge IO, telemetry, and event publication only | provider does not regain native tool JSON projection, identity policy, timeline correlation, or metadata persistence | review-only: pure projection, atomic correlation, and persistence are delegated to focused owners while the provider remains the IO facade | review-only | when OpenClawChatDataProvider is retired |
| chat-conversation-state | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | provider-owned runtime gate and cross-domain state transactions | ChatConversationState | sole lock, timeline and entry metadata, connection/disposal flags, and typed orchestration across lock-free substates; provider supplies bridge context and coordinates IO, telemetry, and events | one authoritative lock atomically commits reset, reconnect, dispose, queue, history, and event transitions without duplicate shared versions | ChatRuntimeOwnershipContractTests.Root_CoordinatesCrossDomainCommitsUnderSoleGate | source-shape | when the chat runtime is replaced by a different atomic transaction boundary |
Expand Down
59 changes: 59 additions & 0 deletions src/OpenClaw.Connection/GatewayConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public sealed class GatewayConnectionManager :
private readonly ICredentialResolver _credentialResolver;
private readonly IGatewayClientFactory _clientFactory;
private readonly GatewayRegistry _registry;
private readonly IGatewayTailscaleAuthLiveVerifier _tailscaleAuthLiveVerifier;
private readonly IOpenClawLogger _logger;
private readonly IDeviceIdentityStore? _identityStore;
private readonly INodeConnector? _nodeConnector;
Expand Down Expand Up @@ -132,6 +133,8 @@ public GatewayConnectionManager(
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_tailscaleAuthLiveVerifier = new GatewayTailscaleAuthLiveVerifier(
new WslExeCommandRunner(_logger));
_identityStore = identityStore;
_nodeConnector = nodeConnector;
_tunnelManager = tunnelManager;
Expand Down Expand Up @@ -203,6 +206,62 @@ public GatewayConnectionManager(
public IOperatorGatewayClient? OperatorClient => _activeLifecycle?.DataClient;
/// <summary>Internal access to the concrete client for auto-approve and other manager-internal operations.</summary>
internal OpenClawGatewayClient? ConcreteOperatorClient => _activeLifecycle?.DataClient;

public async Task<GatewayTailscaleAuthUpgradeResult> EnableTailscaleDashboardAuthAsync(
string gatewayId,
CancellationToken cancellationToken = default)
{
await _transitionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!string.Equals(_activeGatewayRecordId, gatewayId, StringComparison.Ordinal))
return new(GatewayTailscaleAuthUpgradeOutcome.NotActive);

var client = OperatorClient;
if (client is null)
return new(GatewayTailscaleAuthUpgradeOutcome.NotConnected);

var service = new GatewayTailscaleAuthUpgradeService(_registry);
return await service.EnableAsync(
gatewayId,
new GatewayTailscaleAuthConfigClientAdapter(client),
cancellationToken)
.ConfigureAwait(false);
}
finally
{
_transitionSemaphore.Release();
}
}

public async Task<bool> RevalidateTailscaleDashboardAuthAsync(
string gatewayId,
CancellationToken cancellationToken = default)
{
await _transitionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!string.Equals(_activeGatewayRecordId, gatewayId, StringComparison.Ordinal))
return false;

var client = OperatorClient;
if (client is null)
return false;

var service = new GatewayTailscaleAuthUpgradeService(
_registry,
_tailscaleAuthLiveVerifier);
return await service.RevalidateAsync(
gatewayId,
new GatewayTailscaleAuthConfigClientAdapter(client),
cancellationToken)
.ConfigureAwait(false);
}
finally
{
_transitionSemaphore.Release();
}
}
public ConnectionDiagnostics Diagnostics => _diagnostics;

// ─── Lifecycle ───
Expand Down
8 changes: 8 additions & 0 deletions src/OpenClaw.Connection/GatewayRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public sealed record GatewayRecord
/// <summary>WSL distro name for gateway records provisioned by SetupEngine.</summary>
public string? SetupManagedDistroName { get; init; }

/// <summary>
/// True when setup or an explicit upgrade granted this managed gateway's
/// Dashboard access to verified Tailscale identity authentication.
/// </summary>
public bool TrustTailscaleAuth { get; init; }

/// <summary>Per-gateway SSH tunnel configuration. Null if no tunnel needed.</summary>
public SshTunnelConfig? SshTunnel { get; init; }

Expand Down Expand Up @@ -95,6 +101,7 @@ rebuilt.SshTunnel is null &&
// Migrate legacy "Local (<distro>)" ownership to the explicit durable marker.
SetupManagedDistroName = managedDistroName,
RequiresV2Signature = rebuilt.RequiresV2Signature || existing.RequiresV2Signature,
TrustTailscaleAuth = rebuilt.TrustTailscaleAuth || existing.TrustTailscaleAuth,
};
}
else if (existingManagedDistroName is not null)
Expand All @@ -104,6 +111,7 @@ rebuilt.SshTunnel is null &&
IsLocal = OpenClaw.Shared.LocalGatewayUrlClassifier.IsLocalGatewayUrl(rebuilt.Url),
SetupManagedDistroName = null,
RequiresV2Signature = false,
TrustTailscaleAuth = false,
FriendlyName = ParseLegacyManagedDistroName(result.FriendlyName) is not null
? null
: result.FriendlyName,
Expand Down
Loading
Loading