Native Local AI configuration uses authenticated Gateway RPC, not CLI batch
writes. The inspected OpenClaw 2026.9.4 CLI supports batches and expected-current
guards separately; batch validation followed by a write is not compare-and-swap.
LocalAiGatewayRpcConfigurationTransport captures provider, primary and model
allowlist from one valid config.get response and applies only the changed fields
with config.patch { raw, baseHash }. Conflict, disconnect, session replacement
or uncertain response requires reconciliation with the original Gateway. There
is no offline config-file write, blind retry or unguarded rollback.
Gateway config.patch merges object arrays by stable ID and requires explicit
replacePaths for destructive array changes. Withdrawing the owned provider
therefore names only models.providers.llamacpp.models, alongside the same fresh
baseHash; it never grants replacement of another provider's models or the model
allowlist. An UNAVAILABLE response can follow successful persistence when
restart/apply fails, so it remains indeterminate rather than proof of no write.
NativeLocalAiGatewayTarget accepts authenticated isolated native records without
requiring early registry publication. Published connections must still be borrowed
from GatewayConnectionManager; staged connections belong to
NativeGatewaySetupConnection. Remote, WSL and legacy same-user native records
cannot enter this RPC path. Cancellation before dispatch prevents mutation;
cancellation after dispatch drains the bounded request.
The optional runtime authentication substrate keeps a stable current-user DPAPI
credential outside the artifact manifest. The child receives it through
LLAMA_API_KEY, never arguments or the router preset. HTTP clients send Bearer
headers only after loopback validation; RPC publication carries the provider key
inside the authenticated request. WSL callers retain their existing behavior.
LocalAiGatewayLifecycle composes this path with the existing WSL lifecycle.
LocalAI/gateway-binding.json records the exact Gateway endpoint and device
identity, selected model, previous primary, owned allowlist entry and acknowledged
configuration revision. A write-ahead pending flag is flushed before dispatch.
An unacknowledged mutation remains pending across app restarts, with no blind
replay or offline cleanup. A different Gateway cannot adopt the receipt.
Explicit Stop persists AutomaticRecoveryEnabled=false in that same receipt
without relinquishing ownership. Connection notifications use guarded runtime
Resume rather than explicit Start; the runtime operation gate also prevents a
resume already queued behind Stop from restarting the listener. Explicit Start
or Restart re-enables recovery only after successful startup/publication.
New first-Use bindings start disabled, so a clean failure cannot publish on
reconnect. Older receipts default to enabled, preserving
their prior startup behavior.
When stopped intent survives an offline Stop, reconnect performs withdraw-only reconciliation under the runtime operation gate. It never starts the listener, and rechecks durable intent so a queued withdrawal cannot undo a newer Start. Shutdown drains connection-triggered Local AI recovery and disposes the inference runtime before disposing the authorized Gateway connection manager.
After a successful Stop, Release Gateway ownership in Local AI settings explicitly releases the binding. The runtime must be stopped, and a fresh read through the original authenticated identity must confirm absent provider, restored fallback and absent Companion-added allowlist entry. Uncertain writes, offline owners and changed owned fields keep the receipt. Unrelated post-withdrawal edits do not prevent release. Close setup before releasing. Release before switching or removing the original Gateway, then explicitly choose Use on the next Gateway. If already switched, reconnect the original owner first. WSL Use and artifact Repair (including Settings Retry setup) refuse admission while a native binding exists; they cannot reuse a native-authenticated listener with the WSL provider key or replace its shared installation artifacts.
Recovery can confirm an unchanged pre-dispatch revision or an already-withdrawn provider whose primary is the saved fallback or the retained endpoint-cycle model. If a publication landed before a crash, or an unrelated configuration edit changed the whole-config revision while the listener was stopped, explicit Start/Use can recreate only the same authenticated loopback endpoint from the verified installation receipt. This does not replay the Gateway write or change the automatic-port preference. Ownership is confirmed only after exact Gateway inference and an unchanged configuration revision. Port conflicts, changed providers, failed inference and changed identities remain visible failures rather than triggering replacement or credential rotation. If the previous endpoint cannot be recovered, its receipt is retained. Restore the original port and artifacts, or inspect and withdraw the Local AI route using the original Gateway's configuration tools. Matching redacted provider metadata alone is insufficient authority to delete a potentially externally edited route.
Native automatic startup waits for the manager-owned connection and an existing confirmed selection. Observation and artifact acquisition never switch models. Explicit Use publishes the authenticated endpoint, and the existing setup verifier performs real inference with the exact primary, without fallback. A changed revision is not silently adopted: explicit reconciliation requires the same provider/model, successful native inference and an unchanged revision across that probe. Native finalization uses this gate after its capability changes, before publishing the staged Gateway. Cancellation withdraws its route before releasing the native session. Unavailable Gateways and unresolved writes retain their receipt and report cleanup as pending.
A reopened, verified installation with an owned failed listener or unresolved route offers Recover and use Local AI, not artifact Repair. It follows the same guarded endpoint-recovery and inference path. Edited provider, primary or Companion-added allowlist fields are not adopted or overwritten.
Setup also observes the current profile's durable native binding through
LocalAiGatewayLifecycle, so a pending receipt or changed revision can expose
recovery even before the in-memory runtime has rediscovered a failure. This
read-only assessment checks the authenticated route and versioned configuration;
it never starts a listener, settles a receipt, reads or creates an API credential,
or performs inference. Explicit Use still revalidates and executes the existing
guarded recovery. The assessment is not publication authority.
A portable artifact receipt, matching endpoint or successful previous inference
does not prove this profile owns an existing provider. Missing, invalid or
mismatched native ownership keeps management blocked and directs the user to the
original Companion profile and Gateway. No receipt or credential import occurs.
Independently detected models remain usable through the ordinary Gateway choice;
that choice does not restore Companion management. Native duplicate suppression
requires a usable same-owner managed choice, not just a matching artifact model.
Pending ownership and changed revisions keep the independently detected model
visible alongside recovery: neither proves compatibility with current provider
or primary-model fields. Only a confirmed same-owner revision can suppress that
duplicate. Connection loss during inspection retains the native target and
unavailable guidance so Check again can refresh it after reconnection;
caller cancellation still propagates without publishing stale observation.
Verified completion carries an explicit RequiresManagedLocalAi flag from
LocalAiOnboardingUse.Expected, not provider/model equality or destination intent.
Only that managed choice joins runtime recovery and reconciles the native Local AI
receipt during finalization. Ordinary detected choices still reverify the exact
Gateway, identity, session and model through inference, but do not acquire managed
runtime dependencies from a coincidentally matching or pending binding. Fresh
verification and restart receipts preserve this flag. Refresh after an uncertain
managed Use rebuilds the verification client from the retained expectation without
replaying Use, including the consumed install-and-use continuation.
An unavailable configuration read blocks managed setup until a fresh check
succeeds, even on a first installation; unknown provider ownership is not treated
as an empty Gateway. Ordinary detected-model choices remain independent.
File-repair guidance blocks review only while the runtime is owned or its route
is unresolved. Once those conditions clear, the existing guarded Repair flow
remains available; retaining the durable binding alone does not prohibit repair.
Withdrawal can preserve unrelated configuration edits using a fresh CAS revision. An exact unredacted credential or successful inference through the same provider must confirm a still-published route. A redacted credential alone never authorizes deletion: if that listener is unavailable, explicit recovery remains necessary. User-edited allowlist metadata is preserved and its deletion ownership relinquished when routing is safely withdrawn. Native startup completes reconciliation and publication under the runtime gate, including an already-healthy listener; setup never publishes again after that gate has been released. Every admitted non-success Start or Restart, including core startup failure and interrupted completion, keeps recovery disabled and suppresses status-refresh publication and crash restart until explicit Start or Reconcile succeeds. A rejected admission preserves prior intent. Clean failures without a listener or unresolved route still allow read-only refresh to discover a repaired installation, without automatically starting or publishing it. Interrupted Start completion reports a retained listener as failed with unresolved routing, rather than Healthy with disabled recovery. A timestamp-only healthy refresh does not invalidate successful Use; process, endpoint, model evidence and route readiness still must match.
When routing was already withdrawn externally, teardown still removes an unchanged empty allowlist entry owned by Companion through the same write-ahead journal and hash-conditional RPC path. Lost replies retain pending ownership; a fresh read must confirm cleanup before release. User-edited entries remain untouched. Release still requires the documented restored fallback; external primary changes do not relax it.
This document describes the gateway connection system - how the tray app discovers, authenticates with, and maintains connections to OpenClaw gateways.
The normal foreground entry point is WorkspaceWindow, a native WinUI 3 shell
with Reactor chat, assistant selection, Home, sessions, and an Owner
menu. Notifications is an independent footer action immediately beside Owner.
There is no Settings item in the Workspace rail.
The rail is the same native NavigationView as the companion, using its existing
colourful sidebar SVG assets via direct ImageIcon controls. Native menu items
retain their icon-column sizing; the pane uses the expanded theme background
without a second acrylic fill. Session-row selection is retained by original
session key across list refresh, pane toggling, and companion refocus.
Home explicitly selects Home without discarding the current chat draft.
Native menu items
own selection and keyboard behavior; assistant, sessions, and footer controls
use the pane's header and footer slots. Sessions follow Home in the
same native scrolling menu with a native section header. Owner stays fixed
in the footer. Native WinUI TitleBar owns title/icon layout and typography.
NavigationView.PaneHeader puts the pane-collapse button and sidebar-right
Back/Forward in one row, with the assistant selector below. Its final dropdown
option invokes the existing New conversation workflow and restores the selected
agent rather than persisting the action as an agent. It remains disabled while
disconnected or creating a session. The native ComboBox uses transparent/subtle
chrome. Sessions retains its independent subtle Add button.
NavigationView has zero compact width. Home's icon is inline native content
so it is not clipped by WinUI's zero-width icon column. The mode stays Left. Home,
sessions, and the entire footer disappear, and content fills the vacated width.
The native 160ms slide goes directly to zero width; PaneClosed hides offscreen
controls without a second layout step. Native motion preferences still apply.
Both toggle states share a stable overlay position outside the animated pane,
so native focus restoration and intermediate layout cannot move the target.
A floating-style subtle reopen button reserves a dedicated 56 DIP row at
the content's top-left below the titlebar, never covering hosted hit targets.
The pane toggle retains a 40 DIP target and 16 DIP glyph in both states; focus
moves to the surviving toggle. Pane changes do not remount chat or reset drafts.
The companion uses a native TitleBar titled OpenClaw Settings with shared claw
artwork. Search, Back, and Forward live beside the toggle in its stable sidebar
toolbar. The native pane reserves 56 DIP above its items, including in compact mode.
Connection status occupies the footer bar above Diagnostics and Settings;
compact mode retains its icon-only target. Settings has no notification button.
Workspace's bell opens NotificationFlyoutContent using the existing
AppNotificationService, without replacing chat or changing history. Its
explicit Open notifications link and existing deep links retain the full page.
Owner's connection entry shows the same ConnectionStatusPresenter label/accent
as Settings, refreshed both on menu opening and every manager snapshot through
WindowManager, including richer changes that keep the same legacy status.
It opens GatewayStatusContent, shared with Settings,
using the current manager snapshot and existing reconnect/Connection actions.
The flyout contents own named-control application; windows own popup lifetime
and route side effects. Notification subscriptions detach when the flyout closes.
The assistant selector projects the gateway's existing agents.list response:
identity.name (then roster name/ID), identity.emoji, and identity.avatarUrl
(then configured identity.avatar). The gateway resolves workspace-local avatar
files to data URLs for native clients. Windows never reads those paths locally.
AgentIdentityBadge uses native PersonPicture and the existing bounded
MediaResolver for image data and public HTTPS sources; blocked/failed images
are logged and retain emoji/initials. The secondary main-style label is the
agent ID, not a Git branch; this RPC has no branch-name field. Selection honors
defaultId and selectionRequired, without sending a chat or creating a session
when metadata refreshes. Existing item identities/pictures survive roster refresh.
Contract reference: openclaw/openclaw gateway agents-list.ts,
session-utils-store.ts, and packages/gateway-protocol/src/schema/agents-models-skills.ts.
Search retains the existing catalog and keyboard shortcuts through a native
flyout. Both Frame history stacks prune unavailable gateway/diagnostics routes.
All footer
buttons use native SubtleButtonStyle state brushes, and Owner uses the native
PersonPicture avatar rather than a font glyph.
WindowManager owns Workspace and HubWindow independently. The latter is the
Settings companion and reuses the existing native Settings pages. Its Chat rail
item opens or focuses Workspace without selecting a Settings page or replacing
the retained composer. Its activation is queued after NavigationView completes its
own focus handling. WindowManager restores a minimized Workspace and explicitly
requests foreground activation, including when the Chat window is already open.
The Gateway dashboard action and its channels/integrations
description live in their own Connection settings card, not above chat.
Owner's Settings, Usage, Pair device, and About links open or focus that
companion at settings, usage, channels, and about respectively. Closing
the companion does not close Workspace or discard an unsent chat draft.
Generic Workspace refocus preserves the current page; explicit page/session
links still navigate. agent:<id>:workspace retains its original agent-files
meaning and is not the main Workspace route.
Verified native setup completion follows the same window boundary. Chat binds the receipt's exact session to Workspace's retained ChatPage and waits for the native composer before activating the window and consuming the receipt. Channels and Skills keep typed companion navigation. Both paths retain fresh gateway, endpoint, identity, agent, and session checks; selecting a different Workspace destination invalidates an in-flight chat handoff. Admitted assistant changes and new-conversation creation invalidate that binding before any asynchronous session creation, even while the old conversation remains visible. Same-session rebinding must use the new request identity. Cancellation is checked on already-ready paths and immediately before receipt consumption; a canceled launch retains its receipt and restart recovery for explicit retry.
WorkspaceNavigation owns only Home and the footer Notifications destination.
The Home/Sessions-only user correction supersedes the expanded prototype.
WorkspaceNavigationHistory owns Back/Forward history and clears forward
entries only on a different destination. All 20 deprecated workspace: links
(including agents, dashboards, systems, automations, plugins, detail pages,
sessions, and more) explicitly return Home without creating obsolete history
entries. Unknown Workspace routes are not accepted as compatibility aliases.
Both native window entry points use WorkspaceNavigation.Dispatch to reject
unknown prefixed routes before creating a companion or forwarding navigation.
app.navigate reports this rejection as an MCP tool error, not a successful
no-op.
Unprefixed companion routes, including cron, sessions, skills, usage,
and agent:<id>:workspace, are unchanged.
The removed WorkspaceContentPage and WorkspacePageRenderer no longer host
placeholder management pages. Native Cron keeps its existing companion
list/editor and gateway submissions, without a Workspace-specific layout.
WorkspaceProjection still supplies real assistant and conversation identities.
Its sidebar and latest assistant conversation use
SessionDisplayResolver.IsBackground: explicit isBackground wins, otherwise
gateway classification and legacy session keys determine background status.
Home uses the existing Reactor chat and session creation/history; disconnected
state directs users to Connection. Notifications uses the existing notification
service. Owner Settings provides access to the full companion catalog, while
Owner Usage and Pair device keep their deep links. Owner Get apps opens the
existing platforms documentation directly, without a placeholder Apps page.
No underlying management
APIs, companion pages, or capabilities were removed.
The connection event timeline remains an independent ConnectionStatusWindow.
Its initial position is aligned to the right of the active main window's monitor
work area; subsequent activation reuses it without resetting a user's position.
It reads the same connection manager diagnostics, not a parallel client.
An argument-free user launch opens only setup when no gateway is configured
(unless local MCP mode is enabled). After gateway configuration it opens only
Workspace, even when disconnected or node pairing is still pending. Repeat
launches and Workspace tray/deep-link activation refocus required setup instead
of opening Workspace alongside it. Node pairing and credential checks still
gate actual connections; this is only foreground-window selection.
New autostart registrations pass
--background to remain quiet. The installer migrates only exact, argument-free
Run/task entries for its own executable and preserves customized entries and
task enablement. A portable/manual binary replacement bypasses that migration;
re-enable Start with Windows once in Settings to refresh a legacy entry.
Explicit protocol and post-setup restart behavior is unchanged.
Packaged Windows StartupTask activations retain their activation kind through
initial launch and secondary-instance forwarding. Unlike a real interactive
argument-free launch, StartupTask does not implicitly open Workspace. Existing
first-run setup and restart guards remain in effect.
Chat response notifications are suppressed only while native Workspace is visible, not minimized, and showing Home/chat, or the legacy compact chat is visible. An existing Settings companion, a hidden/minimized Workspace, or the Notifications destination is not evidence that chat is visible. The chat and per-type notification settings still apply.
New Workspace resource keys exist in every supported locale. Non-English copy is explicitly deferred English pending translation; resource-key parity is still enforced. Native Light/Dark disconnected proof and synthetic composer regressions are distinct from connected gateway proof. In-process captures do not include the native window frame or Mica; system high contrast requires its own approved host/session and must not be inferred from Dark mode.
Connection management lives in three layers:
OpenClaw.Shared (net10.0) - WebSocket transport, gateway protocol, device identity
↑
OpenClaw.Connection (net10.0) - connection lifecycle, registry, credentials, state machine
↑
OpenClaw.Tray.WinUI (net10.0-windows) - UI app, tray icon, pages, windows
OpenClaw.Shared owns the low-level gateway clients (OpenClawGatewayClient, WindowsNodeClient, WebSocketClientBase), device identity/signing (DeviceIdentity), protocol models, and the IOperatorGatewayClient interface.
WindowsNodeClient also owns gateway invocation lifetime at the transport
boundary. Active invokes are registered by invoke ID in a focused cancellation
registry, linked to the node connection lifetime, and cancelled individually by
the gateway node.invoke.cancel event. Active invocations atomically transition
to cancelled or completed when capability execution returns; whichever
transition wins determines the protocol outcome. Capability implementations
remain responsible for cooperative cancellation of their own underlying work.
OpenClaw.Connection owns all connection management. GatewayConnectionManager
is the public lifecycle façade and sole writer of the overall state machine,
operator lifecycle, active gateway context, tunnel, and operator reconnect
orchestration. Three narrower owners sit behind it:
NodeConnectionCoordinatorowns node generation/CTS/start ordering, connector event handling, node token-mismatch recovery, and node connection telemetry.BootstrapTokenLifecycleowns bootstrap/shared/device handoff timing, the durable two-role token clear gate, and operator token-mismatch recovery timing.DevicePairApprovalCoordinatorowns typed device role-upgrade approval and its one-in-flight plus one-queued bounded node reconnect workflow.
GatewayRegistry, CredentialResolver, ConnectionStateMachine,
NodeConnector, SshTunnelService/Manager, SetupCodeDecoder, and connection
interfaces/DTOs/enums remain separate. This project has zero WinUI dependencies
and is independently testable.
Native Check/Next keeps device-token precedence. Only a typed
AUTH_DEVICE_TOKEN_MISMATCH may trigger one recovery in the disposable
GatewayValidationIdentity: recheck trusted transport and owned-listener
provenance, clear its rejected operator token, then resolve shared before
bootstrap. The saved identity, keypair and original compare-and-swap baseline
are not changed by Check. Successful bootstrap authentication retains its
replacement operator token in memory for Next instead of replaying bootstrap.
Wrong shared tokens, plain remote WebSocket endpoints, ambiguous listeners and
repeated mismatch cannot cause additional credential fallback.
Native automatic recovery also rejects unowned manual-loopback listeners.
Explicit manual-loopback connection is unchanged; the existing non-native
recovery owner retains its prior admission policy through the shared policy's
explicit legacy allowance. Disposable identity copies use the product's
sensitive-file ACL writer, not inherited copy permissions.
Successful native-editor commit results carry the committed Gateway ID and
GatewayDashboardBinding, captured from the transaction candidate rather than a
later active selection. SetupAccessDraft retains both through capability
selection. AI admission checks them before creating an operator client or
borrowing a native manager client; changing the active Gateway, endpoint,
runtime contract or SSH realm cannot silently redirect onboarding.
During an expected model-activation restart, setup validates its captured registry/endpoint binding and persisted signing identity without requiring live handshake fields that disconnect deliberately clears. The bounded wait must finish on a fresh matching authenticated route. Normal setup requests and verification still require connected admin scope and the full live route, with their existing post-await checks. No activation is replayed to recover downtime.
Canceling a Check, or a Next with confirmed rollback, retains the same draft's staged keypair and authenticated replacement token. Draft edits and close discard it; completed or uncertain commits also discard it. No ambiguous transaction result can be reused as a validated draft.
Setup's persisted-registry snapshot comparison ignores only LastConnected,
which can differ briefly between a connection's Update and Save. The snapshot
retains canonical in-memory records; active gateway, credentials, endpoint and
other configuration differences still reject admission.
Reconciliation additionally requires the operation-produced expected output
snapshot. The pipeline checks its prior expected state before reading/writing
registry changes and carries its own resulting snapshot to the UI; it does not
derive authority by rereading disk immediately before adoption.
Pipeline settlement runs after execution and rollback on success, failure, cancellation, and window close. It adopts only the operation's known final registry output against its admitted baseline, refreshes the persistence baseline, and publishes changes outside locks. Stale live connections are disconnected conditionally against their captured connection snapshot; a newer connection is not canceled. External conflicts preserve live edits and provide an explicit reopen/reload recovery message instead of weakening save CAS.
If settlement itself fails, SetupPipelineSettlementException retains the
original PipelineResult or thrown exception alongside the settlement error.
This includes cancellation, failed-step identity, compatibility details and
restart requirements. Progress reports both outcomes and remains failed; a
reconciliation error cannot replace the original diagnostics or become success.
Direct-connect commit and rollback use admitted registry snapshots. If rollback loses a CAS race, it observes the actual persisted active selection and reconciles only that selection, never reasserting the stale candidate. Unreadable state remains unknown with attention required, no guessed settings or old connection restore. Identity preparation failures before transaction admission remain retryable and cannot be reported as committed.
After a failed initial commit, cleanup removes a newly copied candidate identity only when no live or persisted record adopted its ID and its sole key file still matches this operation's copy. The registry lease spans that final absence check and removal. Copy publication returns an exact-content creation transaction: the absence check, baseline calculation and write share the existing identity mutex. Cleanup acquires that same identity mutex inside the registry lease for comparison and deletion. No unlocked post-copy read can adopt a newer writer's bytes as the cleanup baseline. Existing identities, changed files, reparse paths and unknown persisted state are preserved.
PersistenceFileLease serializes cooperating settings/registry writers by
normalized path across instances and local processes. Registry Load/Save,
UpdateAndSave, setup's expected-output Save and reconciliation share that lease.
The final persisted-snapshot comparison and atomic replacement happen within
one lease. A stale writer must reload after a conflict; it cannot overwrite a
new endpoint, credential, active selection or record addition. LastConnected
alone merges monotonically for unchanged authorities.
Hosted setup applies only its owned fields through ISettingsStore, including
the background pipeline settings save. It rejects conflicting same-field edits
while preserving unrelated app.settings.set changes. Standalone setup uses the
same path lease for read/merge/replace. SettingsManager additionally checks its
last loaded/saved JSON before replacing the file, rolls back failed store edits,
and publishes notifications after releasing the file lease.
A stale SettingsManager raises a typed persistence conflict for both the
best-effort Save() path and throwing store updates. It does not adopt the new
disk baseline or overwrite another writer. SettingsPersistenceNotification
projects that state through the UI dispatcher into one persistent error notice.
The notice directs the user to exit OpenClaw from the tray menu, reopen it, and
retry their changes. Repeated failures do not stack notifications or emit
Saved; ISettingsStore.Update still rolls back and throws. Explicit Load()
refreshes both data and baseline and clears the conflict notice, without
automatically restarting the app or replaying edits.
OpenClaw.Tray.WinUI consumes the connection layer through interfaces. It never creates gateway clients directly - GatewayConnectionManager owns that entirely.
The tray app interacts with three main objects:
// Lifecycle
ConnectAsync(gatewayId?) // connect to active or specified gateway
DisconnectAsync() // tear down all connections
ReconnectAsync() // disconnect + connect
SwitchGatewayAsync(gatewayId) // switch to different gateway (stops tunnel, resets state)
ApplySetupCodeAsync(setupCode) // decode QR/setup code → register → connect
// State
CurrentSnapshot // immutable GatewayConnectionSnapshot
OperatorClient // IOperatorGatewayClient for sending gateway requests
ActiveGatewayUrl // which gateway we're connected to
Diagnostics // ring buffer of connection events
// Events
StateChanged // snapshot updated → UI refreshes tray icon, status
OperatorClientChanged // client swapped → rewire data event handlers
DiagnosticEvent // timeline entry for Connection Status windowGetAll() / GetById(id) / GetActive() // read configured gateways
AddOrUpdate(record) // create or update a gateway record
SetActive(id) // switch which gateway is active
FindByUrl(url) // lookup by URL (deduplication)
Save() / Load() // persist to gateways.json
GetIdentityDirectory(id) // per-gateway identity directory path
MigrateFromSettings(...) // one-time legacy migrationThe operator client is received through the OperatorClientChanged event. The app subscribes to data events (sessions, nodes, usage, config, pairing, models, agents, etc.) and calls request methods for chat, node invocations, and configuration.
Inbound chat and agent timeline events must include the gateway's canonical sessionKey. The tray client must not synthesize a literal main key for keyless inbound events, because that can merge unrelated events into the wrong timeline. When a keyless chat or agent event arrives, the tray drops it and raises a one-shot diagnostic so the protocol issue is visible without exposing the dropped message contents.
1. Create GatewayRegistry(SettingsManager.SettingsDirectoryPath)
2. Load gateway registry from gateways.json
3. Create CredentialResolver(DeviceIdentityFileReader.Instance)
4. Create GatewayClientFactory()
5. Create ConnectionDiagnostics()
6. Create NodeConnector(logger, diagnostics)
7. Wire NodeConnector.ClientCreated → NodeService.AttachClient
8. Create SshTunnelService(logger)
9. Create GatewayConnectionManager(resolver, factory, registry, logger,
identityStore, nodeConnector, node mode flag,
diagnostics, tunnelService)
10. Subscribe to OperatorClientChanged → wire/unwire 25+ data event handlers
11. Subscribe to StateChanged → update tray icon + hub window
12. Ensure NodeService exists before gateway initialization
13. Call InitializeGatewayClient() → connects to active gateway
Settings changes are classified by SettingsChangeClassifier.Classify() which compares ConnectionSettingsSnapshot before/after to determine the minimum reconnect action:
| Impact | Action |
|---|---|
NoOp |
Nothing |
UiOnly |
Nothing (UI preferences only) |
CapabilityReload |
Reload node capabilities |
NodeReconnectRequired |
Reconnect node only |
OperatorReconnectRequired |
Reconnect operator (SSH tunnel changed) |
FullReconnectRequired |
Full tear down and reconnect (gateway URL changed) |
ConnectionStateMachine (internal) drives state transitions for both operator and node roles:
Idle → Connecting → Connected
→ PairingRequired → (approved) → Connected
→ Error → (reconnect) → Connecting
→ RateLimited
OverallConnectionState is derived from both roles:
| Operator | Node | Overall |
|---|---|---|
| Error | * | Error |
| PairingRequired | * | PairingRequired |
| Connected | Connected | Ready |
| Connected | Error/Rejected | Degraded |
| Connected | PairingRequired | PairingRequired |
| Connected | Connecting | Connecting |
| Connected | Idle while Node mode is intended | Degraded |
| Connected | Disabled/Off | Ready |
GatewayConnectionSnapshot.NodeConnectionIntended records the Node mode intent used by the manager's state machine. If Node mode is enabled but node startup is skipped, blocked, or missing a node credential, the manager publishes a blocked node snapshot (NodeState=Error, NodeError=...) instead of leaving the node idle and letting tray surfaces report a healthy connection.
GatewayConnectionManager.CurrentSnapshot is the lifecycle truth. Tray/UI state
must treat AppState.Status / ConnectionStatus as a derived compatibility
projection only, produced from the manager snapshot by
ConnectionStatusPresenter. New connection diagnostics should read
GatewayConnectionSnapshot, GatewayRegistry, and ConnectionDiagnostics
directly instead of writing a second runtime model.
Current derived compatibility debt:
| Surface | Status | Notes |
|---|---|---|
AppState.Status |
Derived read-side adapter | The only writer is the manager StateChanged handler, which maps the snapshot through ConnectionStatusPresenter for older UI consumers. |
ConnectionStatus enum |
Retained | Still used by shared gateway/client and tray read-side surfaces. Do not remove it until protocol/client and UI consumers are separated in a smaller migration. |
| Command Center / tray projections | Mixed | New diagnostics use snapshot-derived DTOs. Some older warnings still read AppStateSnapshot.Status; those reads are compatibility gates, not lifecycle ownership. |
The local MCP app.connection.status command is the agent-facing projection of
this model. It reports effective mode/state, active gateway metadata,
operator/node credential resolution, MCP runtime state, browser-proxy caveats,
pending approval actions, retry hints from diagnostics, and recent diagnostic
events without exposing token values.
GatewayRegistry is the source of truth for configured gateways:
%APPDATA%\OpenClawTray\gateways.json - gateway records
%APPDATA%\OpenClawTray\gateways\<id>\ - per-gateway identity directory
%APPDATA%\OpenClawTray\gateways\<id>\device-key-ed25519.json - keypair + tokens
Each GatewayRecord contains: Id, Url, FriendlyName, SharedGatewayToken, BootstrapToken, LastConnected, SshTunnel config, IsLocal, RequiresV2Signature, SetupManagedDistroName, and BrowserControlPort. The IdentityDirName property is computed from Id.
Many gateway records may be saved, but only ActiveId in gateways.json is the effective gateway. Active gateway changes must be made through GatewayRegistry.SetActive(...) and saved immediately by connection flows that switch or apply credentials. SetActive(...) raises GatewayRegistry.Changed, so UI and diagnostics can observe a gateway switch even before the new connection finishes. Each active gateway resolves identity from %APPDATA%\OpenClawTray\gateways\<id>\; old gateway events are ignored by GatewayConnectionManager generation + gateway-id guards after a switch.
SettingsManager still owns general tray settings (node mode, MCP mode, SSH tunnel toggles, notifications, UI preferences). It may read legacy Token / BootstrapToken JSON fields into memory for migration, but save must not write those legacy credential fields back.
GatewayDirectConnectService is the single transaction owner for direct-connect UI surfaces. It commits the registry and active id, applies identity changes, persists SettingsManager, waits for a terminal manager state, and rolls back ordinary asynchronous connection failures as well as thrown failures. When the operation replaced a live operator connection, successful rollback reconnects that previous gateway before returning the failure. The Connection page and Connection Status window only validate controls and render the result. MCP shared-token replacement keeps its device-token-preserving validation semantics, then asks this service to synchronize the committed active gateway into settings and the runtime tunnel.
SetupNativeConnectionPage retains an immutable SetupNativeConnectionRequest.
Its ISetupNativeConnectionHost is composed by WindowManager, using the same
GatewayDirectConnectService instance as the existing Connection surfaces.
It does not create a second connection manager or write settings itself.
Check connection uses GatewayConnectionValidator with a disposable identity
copy, no handshake-token persistence and no reconnect. SSH checks use a separate
owned tunnel on an isolated port, with generation checks again at authentication.
Exact bootstrap-scope and signature compatibility fallbacks use at most two
additional fresh clients with endpoint authorization repeated, not unrestricted
transport reconnect.
Managed-local strong credentials still require the connection manager's
provenance authorization. Setup-code addresses must match an explicitly edited
address; bootstrap tokens never become shared tokens.
The setup host retains that temporary key for the same immutable draft across Check, approval retry and Next. Successful handshake credentials are retained only in setup-owned memory, so a consumed bootstrap code is upgraded to explicit device-token authentication for revalidation. They are not written into the temporary identity file. A draft change or editor close discards the temporary key and in-memory credentials; a successful Next writes them only into the transaction's candidate identity. Temporary key files do exist during editing, but the previous saved identity remains untouched.
Next always repeats validation against the current draft before mutating saved or active state. For the same credential realm, it retains the logical gateway ID, Local AI ownership and all ID-keyed state. The validated identity is promoted using the canonical identity lock and atomic writer, with a snapshot and compare-and-swap rollback. Paired device credentials retain precedence over shared and bootstrap tokens. A newer identity writer is preserved and reported as an incomplete rollback, not silently overwritten.
A changed address or SSH endpoint never inherits the old credentials. Native onboarding adds that realm as a separate gateway and retains the prior saved gateway and identity. The existing Direct editor's replacement semantics are unchanged. Failed or cancelled connection attempts restore the old record, settings and live connection, and discard the candidate identity when newly created. Rollback errors remain failures even if a candidate remains committed. Incomplete rollback or cleanup also reaches a persistent host notification and Connection settings, even if the editor has already closed. Operator pairing-pending is not AI-ready.
When the editor has no saved gateway ID, native Check, identity staging and Next share one lookup by logical URL and SSH credential endpoint before the ordinary URL fallback. This reuses the correct saved identity when several SSH gateways have the same public or loopback URL. Direct editing keeps its existing lookup.
Next is the explicit commit boundary. After it succeeds, later setup cancellation
may retain the chosen gateway. Setup config receives only the effective endpoint;
the existing SetupGatewaySession reads credentials from the active registry.
No browser-profile credentials are accepted by this port, and it does not change
Node mode, local MCP, capability settings, or install WSL.
Credential resolution order is intentionally strict:
- Stored device token in the per-gateway identity directory.
GatewayRecord.SharedGatewayToken- shared token for HTTP/chat surfaces.GatewayRecord.BootstrapToken- one-time setup, limited scopes.- No credential - caller logs and skips client init.
The invariant is that a paired device token always wins. Do not downgrade a paired operator or node to a shared/bootstrap token, because that can reduce scopes or trigger unnecessary re-pairing.
CredentialResolver implements the precedence for WebSocket connections (operator and node roles). It also returns a detailed GatewayCredentialResolution so the active snapshot and diagnostics can distinguish Resolved, Missing, Unreadable, Corrupt, FallbackUsed, and BootstrapRequired. Shared-token-only gateways are a clean resolved state when no paired device token exists. If a stored per-gateway device token is unreadable or corrupt and the resolver falls back to a shared/bootstrap token, GatewayConnectionSnapshot preserves that fallback status instead of reporting only the token source.
Unreadable/corrupt identity fallback is a credential-resolution diagnostic, not permission to replace an existing keypair. A readable stored device token still always wins. When the per-gateway identity file cannot be read or parsed, resolution may identify a same-gateway shared/bootstrap credential, but gateway client construction fails closed until the persisted identity is readable or the user explicitly resets pairing. OpenClaw never regenerates, overwrites, or otherwise changes the identity path on a load failure. The snapshot and diagnostics report the persisted-identity error so UI and diagnostics can prompt repair or explicit re-pair. Credential reads never fall back to another gateway's identity directory.
Node credential precedence follows the same invariant with a distinct stored token:
- Stored node device token in the per-gateway identity directory.
GatewayRecord.SharedGatewayToken- shared token fallback when no paired node token exists.GatewayRecord.BootstrapToken- one-time setup, limited scopes.- No credential - caller logs and skips node client init.
InteractiveGatewayCredentialResolver resolves credentials for HTTP surfaces (chat URL ?token= auth). It prefers SharedGatewayToken over DeviceToken because HTTP endpoints expect the shared token, not the per-device WebSocket token. Browser proxy diagnostics should treat the missing shared token as a browser-control caveat, not as proof that the operator or node gateway connection is disconnected.
The legacy web-chat readiness probe bypasses the process proxy only for loopback
URLs; remote HTTPS gateways retain proxy support. It accepts the original
200-399 response without following redirects, so a readiness check never
contacts a Location destination. Browser navigation remains a separate step.
Two orthogonal self-healing behaviors keep the connection reliable without dead-ending the user:
The gateway may reject a stored device token with the structured code AUTH_DEVICE_TOKEN_MISMATCH (a rotated/revoked/replaced device token) - distinct from a wrong shared token. GatewayErrorClassifier is the single classifier for this: ClassifyWithCode(message, ...codes) inspects the structured error.code/error.details.code before the textual heuristic and returns the exact GatewayErrorKind.DeviceTokenMismatch, keeping a stale device token (auto-recoverable) separate from a wrong shared token (Auth, not device-recoverable). Broad GatewayErrorKind.TokenDrift remains a manual re-pair signal for UI copy.
On a device-token mismatch, the applicable credential owner clears only the
rejected role's device token and reconnects, letting CredentialResolver fall
back to the same record's SharedGatewayToken (preferred) or
BootstrapToken. BootstrapTokenLifecycle owns operator recovery;
NodeConnectionCoordinator owns node recovery from the connector's classified
INodeConnectorTelemetryEvents.ConnectionFailure(GatewayErrorKind). The
manager subscribes once and forwards typed connector events. The node owner
captures lifecycle plus node generation and is the only implementation of the
combined current-node-attempt check. A per-gateway, per-role attempt guard
(reset on handshake success / node pairing) prevents
clear→reconnect→mismatch loops.
Security - trust gate and endpoint provenance. Clearing a device token downgrades to the more powerful shared/bootstrap credential, so IsRecoverySafeEndpoint restricts recovery to trusted endpoints: an owned SSH tunnel, a validated TLS (wss/https) endpoint, or a setup-managed WSL loopback gateway proven by ManagedLocalGatewayPortProvenanceService. The managed-local proof accepts either the existing verified Windows WSL relay identity or a relayless mirrored-networking endpoint with a complete empty Windows listener snapshot, positive expected-distro systemd MainPID ownership, and an immediate second complete empty snapshot. Strong credentials repeat that relayless proof immediately before use. Loopback is not treated as identity by itself: incomplete capture or any unknown, conflicting, or changed Windows listener blocks fallback, so a wrong local process cannot return a device-token mismatch to induce disclosure of the shared credential. A plain ws:// remote endpoint is never eligible.
For an app-owned setup-managed local WSL gateway (WslKeepAlivePolicy.IsSetupManagedLocalRecord - never SSH/remote/ambiguous-localhost), the tray owns process supervision, keeping it out of the connection layer. ManagedLocalGatewayAutoRepairMonitor watches the operator connection and, when it is positively transport-unreachable (GatewayErrorKind.Network/Server, plus a cold-start Connecting state with no failure yet; never unknown/auth/pairing/rate-limit/scope/TLS/tunnel/token-drift) for a sustained window, invokes ManagedLocalGatewayRepairCoordinator. A typed LocalPortConflict is also repairable because its remediation is provenance-gated rather than a blind process restart. The monitor honors a startup grace (so a slow WSL cold start is not interrupted), a per-gateway unhealthy threshold and cooldown, a manager-owned explicit disconnect/stop intent, and a settings kill switch (SettingsData.EnableManagedLocalGatewayAutoRepair, default on).
Default-on product contract and macOS parity. App-installed local gateways are supervised by default for both fresh setups and upgrades, matching the macOS local-mode contract where launchd supervision is active unless OpenClaw is paused. Fresh Windows setup writes EnableManagedLocalGatewayAutoRepair=true explicitly; an existing settings file that predates the field deserializes to the same default. This enrollment is restricted to records whose setup-managed ownership is positively linked to the installed endpoint. Manual localhost, repointed, SSH, and remote records are never adopted. The user-facing controls are Disconnect and Stop on the Connection page: either records explicit operator intent and suppresses automatic restart, process remediation, and reconnect until the operator explicitly connects/starts again. An explicitly persisted false remains available as a policy/debug kill switch and is never overwritten by setup merge.
ManagedLocalGatewayRepairCoordinator probes before it restarts: if the gateway is already reachable it just reconnects (the macOS "attach" path); only a genuinely-down gateway triggers a WSL distro restart (via WslGatewayController), a keepalive re-arm (WslGatewayKeepAliveService.TryEnsureAsync), and a reconnect. For the native-vs-WSL collision case, ManagedLocalGatewayPortProvenanceService classifies listeners by address and proves process command line plus scheduled-task/profile lineage. Relayless mirrored networking is accepted only when complete Windows captures remain empty around positive expected-distro systemd MainPID proof. It automatically disables/stops only a fully proven obsolete native OpenClaw gateway; an unknown, incomplete, or conflicting listener is never trusted or killed and produces precise LocalPortConflict diagnostics. The shared lifecycle lease serializes that destructive work with manual WSL actions. Reconnect is gateway-pinned, intent-aware, and cancellable (GatewayConnectionManager.ReconnectIfCurrentAsync(gatewayId, ct)), so gateway switches, explicit Disconnect/Stop, and shutdown always win. Repair is single-flight, verifies success by a real operator connection to the same gateway, is per-gateway restart-budget-bounded, and never reads or logs credentials.
Operator client (OpenClawGatewayClient): Single instance at a time, owned by GatewayConnectionManager. Created via GatewayClientFactory.Create(). Old instance disposed before creating new one. OperatorClientChanged event notifies consumers of swaps.
Node client (WindowsNodeClient): Two mutually exclusive creation paths:
- Normal:
NodeConnectorcreates it → firesClientCreated→NodeService.AttachClient()receives it (no new client created) - Local setup:
NodeService.ConnectAsync()creates its own client (used only during WSL local gateway setup)
Both paths dispose old clients before creating new ones.
Setup codes (from QR scan or paste) decode to { url, bootstrapToken } via SetupCodeDecoder. The flow:
ApplySetupCodeAsync(code)decodes and validates the gateway URL and bootstrap token- Creates/updates and persists the active
GatewayRecord, preserving any shared token and durable per-role device tokens - Disconnects the previous connection only after the record is durable
BootstrapTokenLifecycleforcesauth.bootstrapTokenfor this connection attempt without clearing stored device tokens; the record-scoped force flag is consumed or cleared even when identity loading fails- After successful pairing, the gateway returns
hello-ok.auth.deviceTokenandBootstrapTokenLifecyclepersists the replacement role token - If pairing or connection fails, the previously stored device tokens remain intact, so retrying or returning to the prior pairing does not require an unintended full re-pair
The bootstrap token is cleared only after operator and node role tokens are both durably readable.
Approval boundaries: GatewayConnectionManager leaves node-pair command-trust requests and reapproval pending for explicit operator approval. It may automatically approve and reconnect only an explicitly typed device-pair request used for a device role upgrade.
In-chat exec approval cards sanitize the gateway's command and message before rendering. If either cannot be reviewed in full because it is truncated, suppressed, or conceals command syntax, the card offers only Deny; the chat provider also rejects Allow RPCs that are not permitted by the matching pending card.
The approval popup retains its original compact lock header, spacing, scrollable identity/access card and icon actions. Reject and Decide later remain grouped on the left, with Approve on the right. Long access descriptions wrap within the card instead of overflowing horizontally. This presentation does not change the approval delay or decision flow.
When another device or node requests pairing, the gateway broadcasts device.pair.requested / node.pair.requested to operators with pairing scope. OpenClawGatewayClient refreshes the pending lists and raises DevicePairListUpdated / NodePairListUpdated, which GatewayService forwards via its PairListsChanged event.
PairingApprovalCoordinator (tray) reconciles those snapshots through the pure PairingApprovalQueue (OpenClaw.Connection) into add/resolve deltas, de-duplicating, suppressing already-decided requests, and filtering out the local node's own pending request (handled by the auto-approve path above). For genuinely new requests - when ShowPairingApprovalDialog is enabled and the operator holds pairing scope - it raises ApprovalRequested, and the app presents a focused PairingApprovalDialog plus an awareness toast (with a "Review" action). The dialog shows the requester's identity and the operator scopes being granted (mapped to friendly text by PairingScopeDescriptions), with Approve / Reject / Decide-later. Approve is briefly disabled on each new request to prevent click-through. Approve/Reject call the IOperatorGatewayClient.{Device,Node}Pair{Approve,Reject}Async RPCs; the queue advances and the dialog closes when empty. The existing Connections-page "Pending approvals" banner remains as the passive fallback when the dialog is disabled. Pure queue/scope logic is unit-tested in OpenClaw.Connection.Tests.
SshTunnelService manages an SSH local port-forward process and implements ISshTunnelManager directly for the connection manager.
When a GatewayRecord has SshTunnel config, the connection manager starts the tunnel before connecting the WebSocket client to ws://localhost:<localPort>. The config stores the SSH daemon port (sshPort, default 22) separately from the remote gateway port forwarded by -L. Startup allows up to 20 seconds for SSH transport, key exchange, authentication, and local-forward binding, while every sample still fails closed on incomplete listener capture or a conflicting loopback/wildcard owner. Same-number listeners bound only to non-loopback interfaces are irrelevant to the local forward and are ignored.
Credential handoff pins the verified tunnel lifecycle generation (or managed-local process identity) from preflight through the initial challenge authorization. If ownership changes between WebSocket acceptance and the credential frame, the current socket is aborted and only a fresh, reauthorized socket may retry.
SshTunnelSnapshot provides a read-only point-in-time view of tunnel state for UI consumption (avoids coupling UI to the mutable service).
EnableMcpServer and EnableNodeMode are independent:
| EnableNodeMode | EnableMcpServer | Behavior |
|---|---|---|
| false | false | Operator-only tray app |
| false | true | Local MCP server only; no gateway required |
| true | false | Gateway node only |
| true | true | Gateway node plus local MCP server |
The EnableMcpServer=true, EnableNodeMode=false path creates a local-only NodeService without requiring a gateway credential.
Tray actions should never silently no-op on common pairing/configuration issues:
- Chat resolves credentials from the active registry record and per-gateway identity. If no usable credential exists, it opens Connection settings instead.
- Canvas opens only when the Windows node is initialized, paired, and the Canvas capability is enabled in settings; otherwise it opens Connection settings.
- Quick Send uses the live operator client and surfaces scope/pairing errors from gateway calls.
system.runandsystem.run.prepareare gated byNodeSystemRunEnabled(defaulttruefor backward compatibility). When disabled, those commands are dropped from advertised capabilities and invocations are rejected.
On first startup with a GatewayRegistry, if no active gateway record exists, the app migrates legacy settings credentials:
LegacyToken→GatewayRecord.SharedGatewayTokenLegacyBootstrapToken→GatewayRecord.BootstrapToken- Old identity file copied into per-gateway identity directory
Migration is idempotent and deduplicates by URL.
The connect handshake uses Ed25519 signatures with v3→v2 fallback:
- Client tries v3 signature first (includes platform and device family)
- If gateway rejects v3, falls back to v2 and remembers for the session
- The
_gatewayNeedsV2Signatureflag persists across reconnects within the sameGatewayConnectionManagerlifetime
Connection tests live in tests/OpenClaw.Connection.Tests/:
ConnectionStateMachineTests- FSM transitions, derived overall stateCredentialResolverTests- credential precedence for operator and nodeGatewayConnectionManagerTests- connect/disconnect/switch, diagnostics, handshakeGatewayRegistryTests/GatewayRegistryMigrationTests- persistence, migrationInteractiveGatewayCredentialResolverTests- HTTP credential resolutionNodeConnectorTests- node client lifecyclePairingFlowTests/NodePairAutoApproveTests- pairing lifecycle, device role-upgrade auto-approval, and manual node command-trust boundarySetupCodeFlowTests/SetupCodeDecoderTests- QR code → connect flowStaleEventGuardTests- generation-guarded event handlingSettingsChangeImpactTests- settings change classificationRetryPolicyTests- backoff policyConnectionDiagnosticsTests- ring buffer diagnosticsNodeConnectionCoordinatorTests- singular node generation and telemetry contractBootstrapTokenLifecycleTests- durable role-token handoff and clear gateDevicePairApprovalCoordinatorTests- bounded device role-upgrade reconnect workflow
The heaviest remaining gap is Windows shell UI behavior (tray clicks, tooltip visibility, WinUI menu routing). Cover pure decision logic in unit tests; use manual or integration smoke tests for shell behavior.