Skip to content
Draft
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
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +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 |
| Capability UI metadata | `NodeCapabilityUiCatalog` (planned) | planned |
| Capability state projection for camera, browser proxy, and system.run | `CapabilityTruthProjection` fed by `NodeCapabilityTruthSource` | authoritative |
| Capability registration/gating | `NodeCapabilityRegistrationPolicy` (planned) | planned |
| Local MCP exposure policy | `McpCapabilityPolicy` (planned) | planned |
| Gateway connect envelope | `ConnectEnvelopeBuilder` (planned) | planned |
Expand Down Expand Up @@ -141,6 +141,7 @@ leading and trailing pipe. Columns, in order:
| connection-page-direct-connect-closed | closed | src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs | direct-connect registry, identity-token, settings, rollback, terminal-wait, and runtime-tunnel transaction | GatewayDirectConnectService | add-form control reads, input validation, result text, and post-success visual refresh only | the page delegates one request; rollback restores the durable registry before identity and settings, reconnects a previously live gateway, and a later credential writer wins | GatewayDirectConnectServiceTests.Connect_Failure_RestoresPreviousLiveConnection | behavioral | when the Connection page no longer contains any direct-connect persistence or rollback logic |
| connection-status-direct-connect-closed | closed | src/OpenClaw.Tray.WinUI/Windows/ConnectionStatusWindow.xaml.cs | direct-connect registry, settings, rollback, terminal-wait, and runtime-tunnel transaction | GatewayDirectConnectService | diagnostics control reads, input validation, and result text only | diagnostics direct connect delegates one request and cannot report success before a terminal manager state | AppRefactorContractTests.StatusWindowDirectConnect_WaitsForManagerStateBeforeReportingConnected | source-shape | when the status window no longer contains direct-connect persistence or rollback logic |
| app-window-manager | planned | src/OpenClaw.Tray.WinUI/App.xaml.cs | window creation/show/hide/shutdown | IWindowManager | composition/delegation only | startup/shutdown ordering deterministic; disposed once | none | review-only | extracted in Phase 3 |
| capability-truth-projection | authoritative | NodeService, Command Center, and app.connection diagnostics | independent interpretations of settings, Windows permission, declaration, approval, and runtime readiness | CapabilityTruthProjection fed by NodeCapabilityTruthSource | existing owners continue to produce facts; projection owns no lifecycle or persistence | camera, browser proxy, and system.run report one fail-closed state to Command Center and local MCP without duplicating GatewayConnectionManager lifecycle | CapabilityTruthProjectionTests.Project_PendingGatewayDeclaration_IsNotReportedReady | behavioral | - |
| app-tray-controller | planned | src/OpenClaw.Tray.WinUI/App.xaml.cs | tray icon/menu/action routing | ITrayController | composition/delegation only | tray actions route unchanged | none | review-only | extracted in Phase 3 |
| app-activation-router | planned | src/OpenClaw.Tray.WinUI/App.xaml.cs | deep-link/toast/single-instance activation | IActivationRouter | composition/delegation only | activation routes land on the same UI/actions; current-user pipe security preserved | none | review-only | extracted in Phase 3 |
| 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 | provider calls the projector while retaining stateful live/history application and metadata cache behavior | 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 | - |
Expand Down
6 changes: 6 additions & 0 deletions docs/MCP_MODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ The implementation is structured so that **adding a new node capability automati

## Architecture

`app.connection.status` exposes an agent-facing `capabilities` projection for
camera, browser proxy, and system.run. It keeps local MCP exposure distinct
from the Gateway's effective declaration and reports Settings, Windows
permission, approval, and runtime readiness without moving connection
lifecycle out of `GatewayConnectionManager`.

### Single capability registry, two transports

![Single capability registry, two transports](diagrams/mcp-mode-dual-transport.svg)
Expand Down
1 change: 1 addition & 0 deletions docs/WINDOWS_NODE_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Every new Windows node call must be exposed through local MCP and `winnode`: reg
- If the gateway omits a safe pending request ID, verify the copy action emits `openclaw nodes pending`, labels it as discovery only, and does not offer reconnect-after-approval yet
- Approve the request explicitly, reconnect the node, and verify the effective capability/command counts update and the pending reapproval warning clears
- Use "Copy fix" only for safe repair commands; privacy-sensitive commands remain informational unless you explicitly opt in on the gateway
- Copy **Capability diagnostics** and verify camera, browser proxy, and system.run each report Settings, Windows permission, Gateway declaration, approval, local MCP exposure, runtime readiness, an overall state, and a repair path. The same entries must appear in `app.connection.status`.

## What Requires Gateway Support

Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.SetupEngine/WindowsNodeContextSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ internal static class WindowsNodeContextSection
public const string Payload = """
This WSL gateway may be paired with the OpenClaw Windows tray node. For Windows desktop, Windows files, screenshots, camera, notifications, browser proxy, or Windows commands, use the `nodes` tool (`status` / `describe`) and target the Windows node instead of assuming the WSL shell can do it.

For Windows shell work, use `exec host=node` / `system.run`; normal gateway exec runs in WSL. If Windows node commands fail, ask the user to check the tray Permissions page: Node mode, System run (or the requested capability), and Exec policy. If settings changed or capabilities look stale, ask the user to reconnect/restart the Windows node or gateway.
For Windows shell work, use `exec host=node` / `system.run`; normal gateway exec runs in WSL. Before claiming that camera, browser proxy, or system.run is unavailable, inspect the Windows Hub capability state with local MCP `app.connection.status` when available. Its `capabilities` entries distinguish the Settings toggle, Windows permission, Gateway declaration, pairing/approval, local MCP exposure, and runtime readiness, and provide the repair path. If the local MCP diagnostic is unavailable, ask the user to copy Capability diagnostics from Windows Hub Command Center. Do not infer camera permission from the Settings toggle alone.
""";

public static string ManagedBlock => $"{BeginMarker}\n{Payload.TrimEnd()}\n{EndMarker}";
Expand Down
220 changes: 220 additions & 0 deletions src/OpenClaw.Shared/CapabilityTruthProjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace OpenClaw.Shared;

/// <summary>
/// Pure, transport-neutral projection of the facts that determine whether an
/// agent can use a Windows-node capability. The owners of settings, Windows
/// permissions, gateway lifecycle, approval, and runtime probes remain
/// unchanged; this type only makes their combined result inspectable.
/// </summary>
public static class CapabilityTruthProjection
{
/// <summary>
/// The declaration gate shared with the projection. Runtime and approval
/// facts may block usability, but they never override an explicit local
/// Settings opt-out by advertising the command anyway.
/// </summary>
public static bool ShouldDeclare(bool settingsEnabled) => settingsEnabled;

public enum WindowsPermissionKind
{
NotRequired,
Unknown,
Allowed,
Denied,
}

public enum ApprovalKind
{
NotConnected,
Unknown,
Approved,
Pending,
Rejected,
}

public enum RuntimeKind
{
Unknown,
Ready,
Blocked,
}

public sealed record Input(
string Id,
string DisplayName,
IReadOnlyList<string> Commands,
bool SettingsEnabled,
WindowsPermissionKind WindowsPermission,
bool GatewaySessionLive,
ApprovalKind Approval,
IReadOnlyCollection<string> EffectiveCommands,
IReadOnlyCollection<string> PendingCommands,
IReadOnlyDictionary<string, bool> GatewayPermissions,
bool LocalMcpExposed,
RuntimeKind Runtime,
string? RuntimeDetail = null,
string? RuntimeRepair = null);

public sealed record State(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("displayName")] string DisplayName,
[property: JsonPropertyName("commands")] IReadOnlyList<string> Commands,
[property: JsonPropertyName("settingsEnabled")] bool SettingsEnabled,
[property: JsonPropertyName("windowsPermission")] string WindowsPermission,
[property: JsonPropertyName("gatewayDeclaration")] string GatewayDeclaration,
[property: JsonPropertyName("approval")] string Approval,
[property: JsonPropertyName("gatewayPathState")] string GatewayPathState,
[property: JsonPropertyName("gatewayRepair")] string GatewayRepair,
[property: JsonPropertyName("localMcpExposed")] bool LocalMcpExposed,
[property: JsonPropertyName("runtimeReadiness")] string RuntimeReadiness,
[property: JsonPropertyName("overallState")] string OverallState,
[property: JsonPropertyName("summary")] string Summary,
[property: JsonPropertyName("repair")] string Repair);

public static State Project(Input input)
{
ArgumentNullException.ThrowIfNull(input);

var gatewayEffective = input.GatewaySessionLive && input.Commands.All(command =>
input.EffectiveCommands.Contains(command, StringComparer.OrdinalIgnoreCase));
var pending = input.Commands.Any(command =>
input.PendingCommands.Contains(command, StringComparer.OrdinalIgnoreCase));
var gatewayPermissionBlocked = input.Commands.Any(command =>
CommandCenterDiagnostics.TryGetCommandPermission(input.GatewayPermissions, command, out var allowed) && !allowed);

var declaration = gatewayEffective
? "effective"
: pending
? "pending-approval"
: !input.GatewaySessionLive
? "not-connected"
: "not-declared";

if (!input.SettingsEnabled)
{
return Build(input, declaration, "disabled",
$"{input.DisplayName} is disabled in Windows Hub Settings.",
$"Enable {input.DisplayName} on the Permissions page, save, and reconnect the Windows node.");
}

if (input.WindowsPermission == WindowsPermissionKind.Denied)
{
return Build(input, declaration, "blocked",
$"{input.DisplayName} is enabled, but Windows permission is denied.",
input.RuntimeRepair ?? "Open Windows privacy settings, grant the required desktop-app permission, and retry.");
}

if (input.Runtime == RuntimeKind.Blocked)
{
return Build(input, declaration, "blocked",
input.RuntimeDetail ?? $"{input.DisplayName} is not runtime-ready.",
input.RuntimeRepair ?? "Repair the runtime prerequisite and retry.");
}

if (input.LocalMcpExposed)
{
var runtime = input.Runtime == RuntimeKind.Unknown ? "unchecked" : "ready";
var summary = input.RuntimeDetail ??
$"{input.DisplayName} is available through local MCP independently of the Gateway declaration.";
var repair = input.Runtime == RuntimeKind.Unknown
? input.RuntimeRepair ?? "Invoke a read-only or safe command to verify the Windows runtime prerequisite."
: "No repair is required.";
return Build(input, declaration, "ready", summary, repair, runtime);
}

if (!gatewayEffective && (pending || input.Approval == ApprovalKind.Rejected))
{
return Build(input, declaration, "pending-approval",
$"{input.DisplayName} is waiting for gateway node command approval.",
"Approve the pending Windows node declaration on the Gateway, then reconnect the node.");
}

if (gatewayPermissionBlocked)
{
return Build(input, declaration, "blocked",
$"{input.DisplayName} is declared, but the Gateway policy blocks one or more commands.",
"Review gateway.nodes allow/deny policy for the listed commands, approve the change, and reconnect the node.");
}

if (gatewayEffective)
{
var runtime = input.Runtime == RuntimeKind.Unknown ? "unchecked" : "ready";
var summary = input.RuntimeDetail ??
$"{input.DisplayName} is effective for the connected Gateway node.";
var repair = input.Runtime == RuntimeKind.Unknown
? input.RuntimeRepair ?? "Invoke a read-only or safe command to verify the Windows runtime prerequisite."
: "No repair is required.";
return Build(input, declaration, "ready", summary, repair, runtime);
}

if (input.GatewaySessionLive)
{
return Build(input, declaration, "blocked",
$"{input.DisplayName} is enabled locally but is not in the Gateway's effective Windows node declaration.",
"Reconnect the Windows node. If the declaration changed, approve the pending command set on the Gateway.");
}

return Build(input, declaration, "unavailable",
$"{input.DisplayName} is enabled, but neither local MCP nor a Gateway declaration currently exposes it.",
"Enable Local MCP Server or connect and approve the Windows node, then retry.");
}

private static State Build(
Input input,
string declaration,
string overall,
string summary,
string repair,
string? runtime = null)
{
var gatewayEffective = input.GatewaySessionLive && input.Commands.All(command =>
input.EffectiveCommands.Contains(command, StringComparer.OrdinalIgnoreCase));
var pending = input.Commands.Any(command =>
input.PendingCommands.Contains(command, StringComparer.OrdinalIgnoreCase));
var gatewayPermissionBlocked = input.Commands.Any(command =>
CommandCenterDiagnostics.TryGetCommandPermission(input.GatewayPermissions, command, out var allowed) && !allowed);
var gatewayPathState = !input.GatewaySessionLive
? pending ? "pending-approval" : "not-connected"
: gatewayPermissionBlocked
? "blocked"
: gatewayEffective
? "ready"
: pending
? "pending-approval"
: "not-declared";
var gatewayRepair = gatewayPathState switch
{
"ready" when pending => "The current declaration remains effective. Approve the pending Windows node changes, then reconnect to activate them.",
"ready" => "No Gateway repair is required.",
"blocked" => "Review gateway.nodes allow/deny policy for the listed commands, approve the change, and reconnect the node.",
"pending-approval" => "Approve the pending Windows node declaration on the Gateway, then reconnect the node.",
"not-declared" => "Reconnect the Windows node. If the declaration changed, approve the pending command set on the Gateway.",
_ => "Connect the Windows node to inspect its Gateway declaration."
};

return new(
input.Id,
input.DisplayName,
input.Commands,
input.SettingsEnabled,
ToKebabCase(input.WindowsPermission.ToString()),
declaration,
ToKebabCase(input.Approval.ToString()),
gatewayPathState,
gatewayRepair,
input.LocalMcpExposed,
runtime ?? ToKebabCase(input.Runtime.ToString()),
overall,
summary,
repair);
}

private static string ToKebabCase(string value) =>
string.Concat(value.Select((character, index) =>
index > 0 && char.IsUpper(character) ? $"-{char.ToLowerInvariant(character)}" : char.ToLowerInvariant(character).ToString()));
}
2 changes: 1 addition & 1 deletion src/OpenClaw.Shared/Mcp/McpToolBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ private object HandleToolsList()
["app.chat.queue.cancel"] =
"Cancel/remove one native chat outgoing queue entry before it is sent. Args: queuedMessageId (string, required), threadId/sessionKey (string, required; use the threadId returned by app.chat.queue.list or app.chat.snapshot). Only Queued/Failed entries can be removed; Sending entries may already have reached the gateway. Returns { canceled, threadId, queuedMessageId, remainingCount, error? }.",
["app.connection.status"] =
"READ-ONLY local MCP connection diagnostics. No args. Returns effective mode/state, active gateway metadata, operator/node credential resolution, MCP runtime state, browser proxy caveat, pending approval actions, retry hints, and recent diagnostic events.",
"READ-ONLY local MCP connection and capability diagnostics. No args. Returns effective mode/state, active gateway metadata, operator/node credential resolution, MCP runtime state, capability truth for camera/browser proxy/system.run, browser proxy caveat, pending approval actions, retry hints, and recent diagnostic events.",
["app.connection.gateways"] =
"READ-ONLY saved gateway diagnostics. No args. Returns { activeGatewayId, count, gateways[] } with per-gateway id/name/url, active flag, lastConnected, credential presence booleans, SSH/browser-proxy configuration, and no token values.",
["app.connection.applySetupCode"] =
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Shared/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,7 @@ public class GatewayCommandCenterState
public GatewaySelfInfo? GatewaySelf { get; set; }
public List<PortDiagnosticInfo> PortDiagnostics { get; set; } = new();
public List<PermissionDiagnosticInfo> Permissions { get; set; } = new();
public List<CapabilityTruthProjection.State> CapabilityStates { get; set; } = new();
public List<ChannelCommandCenterInfo> Channels { get; set; } = new();
public List<SessionInfo> Sessions { get; set; } = new();
public GatewayUsageInfo? Usage { get; set; }
Expand Down
Loading