Skip to content
Merged
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
32 changes: 26 additions & 6 deletions src/OpenClaw.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

internal sealed class CliOptions
{
public string SettingsPath { get; set; } = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenClawTray",
"settings.json");
public string SettingsPath { get; set; } = "";
public bool SettingsPathExplicit { get; set; }
public string? Identity { get; set; }
public string IdentityDataPath { get; set; } = "";

public string? GatewayUrlOverride { get; set; }
public string? TokenOverride { get; set; }
Expand All @@ -33,6 +33,7 @@ private static async Task<int> Main(string[] args)
try
{
options = ParseArgs(args);
ApplyIdentityDefaults(options, Environment.GetEnvironmentVariable);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -64,7 +65,11 @@ private static async Task<int> Main(string[] args)
}

IOpenClawLogger logger = options.Verbose ? new ConsoleLogger() : NullLogger.Instance;
using var client = new OpenClawGatewayClient(gatewayUrl, token, logger);
using var client = new OpenClawGatewayClient(
gatewayUrl,
token,
logger,
identityPath: options.IdentityDataPath);

var lastStatus = ConnectionStatus.Disconnected;
var connectedTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Expand Down Expand Up @@ -218,6 +223,10 @@ private static CliOptions ParseArgs(string[] args)
{
case "--settings":
options.SettingsPath = RequireValue(args, ref i, arg);
options.SettingsPathExplicit = true;
break;
case "--identity":
options.Identity = OpenClawAppIdentity.NormalizeIdentity(RequireValue(args, ref i, arg));
break;
case "--url":
options.GatewayUrlOverride = RequireValue(args, ref i, arg);
Expand Down Expand Up @@ -251,6 +260,16 @@ private static CliOptions ParseArgs(string[] args)
return options;
}

private static void ApplyIdentityDefaults(CliOptions options, Func<string, string?> envLookup)
{
options.Identity = OpenClawAppIdentity.ResolveIdentity(envLookup, options.Identity);
options.IdentityDataPath = OpenClawAppIdentity.ResolveRoamingDataDirectory(envLookup, options.Identity);
if (!options.SettingsPathExplicit)
{
options.SettingsPath = OpenClawAppIdentity.ResolveSettingsPath(envLookup, options.Identity);
}
}

private static string RequireValue(string[] args, ref int index, string name)
{
if (index + 1 >= args.Length)
Expand Down Expand Up @@ -282,7 +301,8 @@ private static void PrintUsage()
Console.WriteLine(" dotnet run --project src/OpenClaw.Cli -- [options]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" --settings <path> Settings file (default: %APPDATA%\\OpenClawTray\\settings.json)");
Console.WriteLine(" --settings <path> Settings file (default: selected identity profile)");
Console.WriteLine(" --identity <release|dev> Select tray profile (default: %OPENCLAW_APP_IDENTITY% or release)");
Console.WriteLine(" --url <ws://...> Override gateway URL");
Console.WriteLine(" --token <token> Override token");
Console.WriteLine(" --message <text> Message to send");
Expand Down
11 changes: 6 additions & 5 deletions src/OpenClaw.Connection/GatewayConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -710,16 +710,17 @@ private async Task<SetupCodeResult> ValidateSharedTokenBeforeReplacementAsync(

private async Task HandleOperatorStatusChangedAsync(ConnectionStatus status, long gen)
{
// Check client's pairing status directly — set synchronously before this handler runs
var isPairingPending = _activeLifecycle?.DataClient?.IsPairingRequired == true;
if (isPairingPending && status is ConnectionStatus.Disconnected or ConnectionStatus.Error)
return;

await _transitionSemaphore.WaitAsync();
try
{
if (Interlocked.Read(ref _generation) != gen) return;

// Check client's pairing status while holding the transition lock so
// a completed pairing cannot race with a stale disconnect/error event.
var isPairingPending = _activeLifecycle?.DataClient?.IsPairingRequired == true;
if (isPairingPending && status is ConnectionStatus.Disconnected or ConnectionStatus.Error)
return;

switch (status)
{
case ConnectionStatus.Connected:
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.SetupEngine/SetupSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3479,7 +3479,7 @@ public override Task<StepResult> ExecuteAsync(SetupContext ctx, CancellationToke
psi.ArgumentList.Add("sleep");
psi.ArgumentList.Add("infinity");

var proc = System.Diagnostics.Process.Start(psi);
using var proc = System.Diagnostics.Process.Start(psi);
if (proc == null)
{
ctx.Logger.Warn("Failed to start keepalive process — tray will start its own");
Expand Down
76 changes: 76 additions & 0 deletions src/OpenClaw.Shared/OpenClawAppIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace OpenClaw.Shared;

/// <summary>
/// Shared profile/path rules for standalone tools that need to find the same
/// release or dev profile used by the tray.
/// </summary>
public static class OpenClawAppIdentity
{
public const string ReleaseIdentity = "release";
public const string DevIdentity = "dev";
public const string IdentityEnvironmentVariable = "OPENCLAW_APP_IDENTITY";
public const string DataDirectoryOverrideEnvironmentVariable = "OPENCLAW_TRAY_DATA_DIR";
public const string AppDataRootEnvironmentVariable = "OPENCLAW_TRAY_APPDATA_DIR";
public const string ReleaseDataDirectoryName = "OpenClawTray";
public const string DevDataDirectoryName = "OpenClawTray-Dev";

public static string NormalizeIdentity(string? identity)
{
if (string.IsNullOrWhiteSpace(identity))
return ReleaseIdentity;

if (string.Equals(identity, ReleaseIdentity, StringComparison.OrdinalIgnoreCase))
return ReleaseIdentity;

if (string.Equals(identity, DevIdentity, StringComparison.OrdinalIgnoreCase))
return DevIdentity;

throw new ArgumentException(
$"App identity must be '{ReleaseIdentity}' or '{DevIdentity}' (got '{identity}').",
nameof(identity));
}

public static string ResolveIdentity(Func<string, string?> envLookup, string? explicitIdentity = null)
{
ArgumentNullException.ThrowIfNull(envLookup);

return NormalizeIdentity(
!string.IsNullOrWhiteSpace(explicitIdentity)
? explicitIdentity
: envLookup(IdentityEnvironmentVariable));
}

public static string GetDataDirectoryName(string? identity) =>
NormalizeIdentity(identity) == DevIdentity
? DevDataDirectoryName
: ReleaseDataDirectoryName;

public static string ResolveRoamingDataDirectory(
Func<string, string?> envLookup,
string? explicitIdentity = null)
{
ArgumentNullException.ThrowIfNull(envLookup);

var dataDirOverride = envLookup(DataDirectoryOverrideEnvironmentVariable);
if (!string.IsNullOrWhiteSpace(dataDirOverride))
return dataDirOverride!;

var root = envLookup(AppDataRootEnvironmentVariable);
if (string.IsNullOrWhiteSpace(root))
root = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

return Path.Combine(
root!,
GetDataDirectoryName(ResolveIdentity(envLookup, explicitIdentity)));
}

public static string ResolveSettingsPath(
Func<string, string?> envLookup,
string? explicitIdentity = null) =>
Path.Combine(ResolveRoamingDataDirectory(envLookup, explicitIdentity), "settings.json");

public static string ResolveMcpTokenPath(
Func<string, string?> envLookup,
string? explicitIdentity = null) =>
Path.Combine(ResolveRoamingDataDirectory(envLookup, explicitIdentity), "mcp-token.txt");
}
18 changes: 8 additions & 10 deletions src/OpenClaw.Shared/OpenClawGatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ public partial class OpenClawGatewayClient : WebSocketClientBase, IOperatorGatew
private readonly bool _ignoreStoredDeviceToken;

/// <summary>True when the gateway reported "pairing required" for this device.</summary>
public bool IsPairingRequired => _pairingRequiredAwaitingApproval;
public bool IsPairingRequired => Volatile.Read(ref _pairingRequiredAwaitingApproval);

/// <summary>Safe requestId returned in structured pairing-required details, when present.</summary>
public string? PairingRequiredRequestId => _pairingRequiredRequestId;
public string? PairingRequiredRequestId => Volatile.Read(ref _pairingRequiredRequestId);

/// <summary>True when the device signature was rejected in all supported modes.</summary>
public bool IsAuthFailed => _authFailed;
Expand Down Expand Up @@ -248,10 +248,8 @@ public OpenClawGatewayClient(string gatewayUrl, string token, IOpenClawLogger? l
_bootstrapPairAsNode = bootstrapPairAsNode;
_ignoreStoredDeviceToken = ignoreStoredDeviceToken;
_currentGatewayUrl = gatewayUrl;
var dataPath = identityPath ?? Path.Combine(
Environment.GetEnvironmentVariable("OPENCLAW_TRAY_APPDATA_DIR")
?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenClawTray");
var dataPath = identityPath ?? OpenClawAppIdentity.ResolveRoamingDataDirectory(
Environment.GetEnvironmentVariable);

_deviceIdentity = new DeviceIdentity(dataPath, _logger);
_deviceIdentity.Initialize();
Expand Down Expand Up @@ -1759,8 +1757,8 @@ private void HandleResponse(JsonElement root)
if (payload.TryGetProperty("type", out var t) && t.GetString() == "hello-ok")
{
_logger.Info($"[HANDSHAKE] Received hello-ok!");
_pairingRequiredAwaitingApproval = false;
_pairingRequiredRequestId = null;
Volatile.Write(ref _pairingRequiredAwaitingApproval, false);
Volatile.Write(ref _pairingRequiredRequestId, null);
_authFailed = false;
ResetReconnectAttempts();
_operatorDeviceId = TryGetHandshakeDeviceId(payload);
Expand Down Expand Up @@ -2067,8 +2065,8 @@ private void HandleRequestError(string? method, JsonElement root)
if (method == "connect" &&
(pairingDetails.IsPairingRequired || message.Contains("pairing required", StringComparison.OrdinalIgnoreCase)))
{
_pairingRequiredAwaitingApproval = true;
_pairingRequiredRequestId = pairingDetails.RequestId;
Volatile.Write(ref _pairingRequiredRequestId, pairingDetails.RequestId);
Volatile.Write(ref _pairingRequiredAwaitingApproval, true);
_logger.Warn($"[HANDSHAKE] Pairing required (requestId={pairingDetails.RequestId}). Waiting for approval.");
PairingRequired?.Invoke(this, pairingDetails.RequestId);
return;
Expand Down
2 changes: 2 additions & 0 deletions src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@
Click="OnRemoveRule" Tag="{Binding Index}"
Background="Transparent" BorderThickness="0"
Padding="6" MinWidth="0"
AutomationProperties.Name="{Binding RemoveRuleAutomationName}"
AutomationProperties.AutomationId="{Binding RemoveRuleAutomationId}"
ToolTipService.ToolTip="Remove rule">
<FontIcon Glyph="&#xE74D;" FontSize="14"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
Expand Down
2 changes: 2 additions & 0 deletions src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,8 @@ private void RefreshPolicyRulesList()
r.Pattern,
Action = DisplayExecPolicyAction(r.Action),
r.Index,
RemoveRuleAutomationName = $"Remove rule {r.Pattern}",
RemoveRuleAutomationId = $"RemoveExecPolicyRuleButton_{r.Index}",
ActionBrush = r.Action == "allow"
? allowBrush
: r.Action == "prompt" ? askBrush : denyBrush
Expand Down
33 changes: 23 additions & 10 deletions src/OpenClaw.WinNode.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using OpenClaw.Shared;
using OpenClaw.Shared.Mcp;

namespace OpenClaw.WinNode.Cli;
Expand All @@ -19,6 +20,7 @@ internal sealed class WinNodeOptions
public string? McpUrlOverride { get; set; }
public int? McpPortOverride { get; set; }
public string? McpTokenOverride { get; set; }
public string? Identity { get; set; }
public bool Verbose { get; set; }
}

Expand Down Expand Up @@ -151,6 +153,16 @@ public static async Task<int> RunAsync(
return 2;
}

try
{
options.Identity = OpenClawAppIdentity.ResolveIdentity(envLookup, options.Identity);
}
catch (ArgumentException ex)
{
stderr.WriteLine($"Argument error: {ex.Message}");
return 2;
}

var token = ResolveAuthToken(options, envLookup, stderr);
if (token.Source == "error")
{
Expand Down Expand Up @@ -533,7 +545,9 @@ private static int ResolveEnvPort(Func<string, string?> envLookup, bool verbose,
/// per-tool secret env-var convention — same shape as <c>GITHUB_TOKEN</c>,
/// <c>ANTHROPIC_API_KEY</c>, <c>NUGET_API_KEY</c>.</item>
/// <item>The on-disk token file the tray writes when MCP is enabled —
/// <c>%APPDATA%\OpenClawTray\mcp-token.txt</c> by default, or
/// <c>%APPDATA%\OpenClawTray\mcp-token.txt</c> by default,
/// <c>%APPDATA%\OpenClawTray-Dev\mcp-token.txt</c> when
/// <c>--identity dev</c> or <c>OPENCLAW_APP_IDENTITY=dev</c> is set, or
/// <c>$OPENCLAW_TRAY_DATA_DIR\mcp-token.txt</c> when the tray was launched
/// with that sandbox override (the integration test fixture uses it).</item>
/// </list>
Expand All @@ -559,7 +573,7 @@ internal static AuthTokenResult ResolveAuthToken(
return new AuthTokenResult(envToken, "OPENCLAW_MCP_TOKEN");
}

var path = ResolveTokenPath(envLookup);
var path = ResolveTokenPath(envLookup, options.Identity);

// F-08: resolve to canonical form and require the result still live
// under the requested directory tree. Defeats a same-user attacker
Expand Down Expand Up @@ -697,21 +711,15 @@ private static bool PathStartsWith(string candidate, string prefix)
|| normCandidate.StartsWith(normPrefix + Path.DirectorySeparatorChar, cmp);
}

internal static string ResolveTokenPath(Func<string, string?> envLookup)
internal static string ResolveTokenPath(Func<string, string?> envLookup, string? identity = null)
{
// Mirror SettingsManager.SettingsDirectoryPath: when the tray was
// launched with OPENCLAW_TRAY_DATA_DIR, settings (including the token
// file) live under that directory. The same env var is honored here
// so a CLI invoked in the same shell as a sandboxed tray Just Works,
// and the integration test fixture can redirect both the producer
// (tray) and the consumer (CLI) with one env var.
var dataDirOverride = envLookup("OPENCLAW_TRAY_DATA_DIR");
var dir = !string.IsNullOrWhiteSpace(dataDirOverride)
? dataDirOverride!
: Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenClawTray");
return Path.Combine(dir, "mcp-token.txt");
return OpenClawAppIdentity.ResolveMcpTokenPath(envLookup, identity);
}

/// <summary>
Expand Down Expand Up @@ -774,6 +782,9 @@ internal static WinNodeOptions ParseArgs(string[] args)
case "--mcp-token":
options.McpTokenOverride = RequireValue(args, ref i, arg);
break;
case "--identity":
options.Identity = OpenClawAppIdentity.NormalizeIdentity(RequireValue(args, ref i, arg));
break;
case "--verbose":
options.Verbose = true;
break;
Expand Down Expand Up @@ -830,6 +841,8 @@ internal static void PrintUsage(TextWriter stdout)
stdout.WriteLine(" --mcp-token <token> Bearer token (testing/explicit overrides only - visible to");
stdout.WriteLine(" other processes via the OS process listing). Prefer");
stdout.WriteLine(" $OPENCLAW_MCP_TOKEN or %APPDATA%\\OpenClawTray\\mcp-token.txt");
stdout.WriteLine(" --identity <release|dev> Select tray profile for default token lookup");
stdout.WriteLine(" (default: $OPENCLAW_APP_IDENTITY or release)");
stdout.WriteLine(" --verbose Print endpoint + ignored flags to stderr");
stdout.WriteLine(" --help, -h Show this help");
stdout.WriteLine();
Expand Down
15 changes: 10 additions & 5 deletions src/OpenClaw.WinNode.Cli/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ argument shape, and the A2UI v0.8 JSONL grammar. It is shipped alongside
## Invocation shape

```
winnode --command <name> [--params '<json-object>'] [--invoke-timeout <ms>]
winnode --list-tools [--mcp-url <url>|--mcp-port <port>]
winnode --command <name> [--params '<json-object>'] [--invoke-timeout <ms>] [--identity release|dev]
winnode --list-tools [--mcp-url <url>|--mcp-port <port>] [--identity release|dev]
```

- `--command` (required) — node command (e.g. `system.which`, `canvas.a2ui.push`).
Expand All @@ -50,9 +50,14 @@ winnode --list-tools [--mcp-url <url>|--mcp-port <port>]
listing** (`Get-CimInstance Win32_Process | Select CommandLine`,
Process Explorer, etc.). The CLI emits a stderr warning when this flag is
used. **Prefer `OPENCLAW_MCP_TOKEN` (env var) or the on-disk
`%APPDATA%\OpenClawTray\mcp-token.txt`** which the tray writes when MCP is
enabled. Both `OPENCLAW_MCP_TOKEN` and the on-disk file should themselves be
treated as sensitive operational secrets.
`%APPDATA%\OpenClawTray\mcp-token.txt`** which the release tray writes when
MCP is enabled. Both `OPENCLAW_MCP_TOKEN` and the on-disk file should
themselves be treated as sensitive operational secrets.
- `--identity release|dev` — selects which tray profile supplies the default
on-disk MCP token. Defaults to `OPENCLAW_APP_IDENTITY`, then `release`.
Use `--identity dev` for a side-by-side dev tray; its default token path is
`%APPDATA%\OpenClawTray-Dev\mcp-token.txt`. `OPENCLAW_TRAY_DATA_DIR` still
wins for isolated runs and points directly at the data folder.
- `--verbose` — log endpoint + ignored flags to stderr. Without `--verbose`,
HTTP error bodies are emitted only as the first line; with `--verbose`, the
full body is shown (after sanitization + token-shape redaction).
Expand Down
Loading