Skip to content

Commit b3a2b4e

Browse files
shanselmanCopilot
andauthored
Stabilize dev identity paths
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent af9edc7 commit b3a2b4e

13 files changed

Lines changed: 395 additions & 37 deletions

File tree

src/OpenClaw.Cli/Program.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
internal sealed class CliOptions
66
{
7-
public string SettingsPath { get; set; } = Path.Combine(
8-
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
9-
"OpenClawTray",
10-
"settings.json");
7+
public string SettingsPath { get; set; } = "";
8+
public bool SettingsPathExplicit { get; set; }
9+
public string? Identity { get; set; }
10+
public string IdentityDataPath { get; set; } = "";
1111

1212
public string? GatewayUrlOverride { get; set; }
1313
public string? TokenOverride { get; set; }
@@ -33,6 +33,7 @@ private static async Task<int> Main(string[] args)
3333
try
3434
{
3535
options = ParseArgs(args);
36+
ApplyIdentityDefaults(options, Environment.GetEnvironmentVariable);
3637
}
3738
catch (Exception ex)
3839
{
@@ -64,7 +65,11 @@ private static async Task<int> Main(string[] args)
6465
}
6566

6667
IOpenClawLogger logger = options.Verbose ? new ConsoleLogger() : NullLogger.Instance;
67-
using var client = new OpenClawGatewayClient(gatewayUrl, token, logger);
68+
using var client = new OpenClawGatewayClient(
69+
gatewayUrl,
70+
token,
71+
logger,
72+
identityPath: options.IdentityDataPath);
6873

6974
var lastStatus = ConnectionStatus.Disconnected;
7075
var connectedTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -218,6 +223,10 @@ private static CliOptions ParseArgs(string[] args)
218223
{
219224
case "--settings":
220225
options.SettingsPath = RequireValue(args, ref i, arg);
226+
options.SettingsPathExplicit = true;
227+
break;
228+
case "--identity":
229+
options.Identity = OpenClawAppIdentity.NormalizeIdentity(RequireValue(args, ref i, arg));
221230
break;
222231
case "--url":
223232
options.GatewayUrlOverride = RequireValue(args, ref i, arg);
@@ -251,6 +260,16 @@ private static CliOptions ParseArgs(string[] args)
251260
return options;
252261
}
253262

263+
private static void ApplyIdentityDefaults(CliOptions options, Func<string, string?> envLookup)
264+
{
265+
options.Identity = OpenClawAppIdentity.ResolveIdentity(envLookup, options.Identity);
266+
options.IdentityDataPath = OpenClawAppIdentity.ResolveRoamingDataDirectory(envLookup, options.Identity);
267+
if (!options.SettingsPathExplicit)
268+
{
269+
options.SettingsPath = OpenClawAppIdentity.ResolveSettingsPath(envLookup, options.Identity);
270+
}
271+
}
272+
254273
private static string RequireValue(string[] args, ref int index, string name)
255274
{
256275
if (index + 1 >= args.Length)
@@ -282,7 +301,8 @@ private static void PrintUsage()
282301
Console.WriteLine(" dotnet run --project src/OpenClaw.Cli -- [options]");
283302
Console.WriteLine();
284303
Console.WriteLine("Options:");
285-
Console.WriteLine(" --settings <path> Settings file (default: %APPDATA%\\OpenClawTray\\settings.json)");
304+
Console.WriteLine(" --settings <path> Settings file (default: selected identity profile)");
305+
Console.WriteLine(" --identity <release|dev> Select tray profile (default: %OPENCLAW_APP_IDENTITY% or release)");
286306
Console.WriteLine(" --url <ws://...> Override gateway URL");
287307
Console.WriteLine(" --token <token> Override token");
288308
Console.WriteLine(" --message <text> Message to send");

src/OpenClaw.Connection/GatewayConnectionManager.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -710,16 +710,17 @@ private async Task<SetupCodeResult> ValidateSharedTokenBeforeReplacementAsync(
710710

711711
private async Task HandleOperatorStatusChangedAsync(ConnectionStatus status, long gen)
712712
{
713-
// Check client's pairing status directly — set synchronously before this handler runs
714-
var isPairingPending = _activeLifecycle?.DataClient?.IsPairingRequired == true;
715-
if (isPairingPending && status is ConnectionStatus.Disconnected or ConnectionStatus.Error)
716-
return;
717-
718713
await _transitionSemaphore.WaitAsync();
719714
try
720715
{
721716
if (Interlocked.Read(ref _generation) != gen) return;
722717

718+
// Check client's pairing status while holding the transition lock so
719+
// a completed pairing cannot race with a stale disconnect/error event.
720+
var isPairingPending = _activeLifecycle?.DataClient?.IsPairingRequired == true;
721+
if (isPairingPending && status is ConnectionStatus.Disconnected or ConnectionStatus.Error)
722+
return;
723+
723724
switch (status)
724725
{
725726
case ConnectionStatus.Connected:

src/OpenClaw.SetupEngine/SetupSteps.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3479,7 +3479,7 @@ public override Task<StepResult> ExecuteAsync(SetupContext ctx, CancellationToke
34793479
psi.ArgumentList.Add("sleep");
34803480
psi.ArgumentList.Add("infinity");
34813481

3482-
var proc = System.Diagnostics.Process.Start(psi);
3482+
using var proc = System.Diagnostics.Process.Start(psi);
34833483
if (proc == null)
34843484
{
34853485
ctx.Logger.Warn("Failed to start keepalive process — tray will start its own");
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
namespace OpenClaw.Shared;
2+
3+
/// <summary>
4+
/// Shared profile/path rules for standalone tools that need to find the same
5+
/// release or dev profile used by the tray.
6+
/// </summary>
7+
public static class OpenClawAppIdentity
8+
{
9+
public const string ReleaseIdentity = "release";
10+
public const string DevIdentity = "dev";
11+
public const string IdentityEnvironmentVariable = "OPENCLAW_APP_IDENTITY";
12+
public const string DataDirectoryOverrideEnvironmentVariable = "OPENCLAW_TRAY_DATA_DIR";
13+
public const string AppDataRootEnvironmentVariable = "OPENCLAW_TRAY_APPDATA_DIR";
14+
public const string ReleaseDataDirectoryName = "OpenClawTray";
15+
public const string DevDataDirectoryName = "OpenClawTray-Dev";
16+
17+
public static string NormalizeIdentity(string? identity)
18+
{
19+
if (string.IsNullOrWhiteSpace(identity))
20+
return ReleaseIdentity;
21+
22+
if (string.Equals(identity, ReleaseIdentity, StringComparison.OrdinalIgnoreCase))
23+
return ReleaseIdentity;
24+
25+
if (string.Equals(identity, DevIdentity, StringComparison.OrdinalIgnoreCase))
26+
return DevIdentity;
27+
28+
throw new ArgumentException(
29+
$"App identity must be '{ReleaseIdentity}' or '{DevIdentity}' (got '{identity}').",
30+
nameof(identity));
31+
}
32+
33+
public static string ResolveIdentity(Func<string, string?> envLookup, string? explicitIdentity = null)
34+
{
35+
ArgumentNullException.ThrowIfNull(envLookup);
36+
37+
return NormalizeIdentity(
38+
!string.IsNullOrWhiteSpace(explicitIdentity)
39+
? explicitIdentity
40+
: envLookup(IdentityEnvironmentVariable));
41+
}
42+
43+
public static string GetDataDirectoryName(string? identity) =>
44+
NormalizeIdentity(identity) == DevIdentity
45+
? DevDataDirectoryName
46+
: ReleaseDataDirectoryName;
47+
48+
public static string ResolveRoamingDataDirectory(
49+
Func<string, string?> envLookup,
50+
string? explicitIdentity = null)
51+
{
52+
ArgumentNullException.ThrowIfNull(envLookup);
53+
54+
var dataDirOverride = envLookup(DataDirectoryOverrideEnvironmentVariable);
55+
if (!string.IsNullOrWhiteSpace(dataDirOverride))
56+
return dataDirOverride!;
57+
58+
var root = envLookup(AppDataRootEnvironmentVariable);
59+
if (string.IsNullOrWhiteSpace(root))
60+
root = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
61+
62+
return Path.Combine(
63+
root!,
64+
GetDataDirectoryName(ResolveIdentity(envLookup, explicitIdentity)));
65+
}
66+
67+
public static string ResolveSettingsPath(
68+
Func<string, string?> envLookup,
69+
string? explicitIdentity = null) =>
70+
Path.Combine(ResolveRoamingDataDirectory(envLookup, explicitIdentity), "settings.json");
71+
72+
public static string ResolveMcpTokenPath(
73+
Func<string, string?> envLookup,
74+
string? explicitIdentity = null) =>
75+
Path.Combine(ResolveRoamingDataDirectory(envLookup, explicitIdentity), "mcp-token.txt");
76+
}

src/OpenClaw.Shared/OpenClawGatewayClient.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ public partial class OpenClawGatewayClient : WebSocketClientBase, IOperatorGatew
100100
private readonly bool _ignoreStoredDeviceToken;
101101

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

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

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

256254
_deviceIdentity = new DeviceIdentity(dataPath, _logger);
257255
_deviceIdentity.Initialize();
@@ -1759,8 +1757,8 @@ private void HandleResponse(JsonElement root)
17591757
if (payload.TryGetProperty("type", out var t) && t.GetString() == "hello-ok")
17601758
{
17611759
_logger.Info($"[HANDSHAKE] Received hello-ok!");
1762-
_pairingRequiredAwaitingApproval = false;
1763-
_pairingRequiredRequestId = null;
1760+
Volatile.Write(ref _pairingRequiredAwaitingApproval, false);
1761+
Volatile.Write(ref _pairingRequiredRequestId, null);
17641762
_authFailed = false;
17651763
ResetReconnectAttempts();
17661764
_operatorDeviceId = TryGetHandshakeDeviceId(payload);
@@ -2067,8 +2065,8 @@ private void HandleRequestError(string? method, JsonElement root)
20672065
if (method == "connect" &&
20682066
(pairingDetails.IsPairingRequired || message.Contains("pairing required", StringComparison.OrdinalIgnoreCase)))
20692067
{
2070-
_pairingRequiredAwaitingApproval = true;
2071-
_pairingRequiredRequestId = pairingDetails.RequestId;
2068+
Volatile.Write(ref _pairingRequiredRequestId, pairingDetails.RequestId);
2069+
Volatile.Write(ref _pairingRequiredAwaitingApproval, true);
20722070
_logger.Warn($"[HANDSHAKE] Pairing required (requestId={pairingDetails.RequestId}). Waiting for approval.");
20732071
PairingRequired?.Invoke(this, pairingDetails.RequestId);
20742072
return;

src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@
329329
Click="OnRemoveRule" Tag="{Binding Index}"
330330
Background="Transparent" BorderThickness="0"
331331
Padding="6" MinWidth="0"
332+
AutomationProperties.Name="{Binding RemoveRuleAutomationName}"
333+
AutomationProperties.AutomationId="{Binding RemoveRuleAutomationId}"
332334
ToolTipService.ToolTip="Remove rule">
333335
<FontIcon Glyph="&#xE74D;" FontSize="14"
334336
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>

src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,8 @@ private void RefreshPolicyRulesList()
711711
r.Pattern,
712712
Action = DisplayExecPolicyAction(r.Action),
713713
r.Index,
714+
RemoveRuleAutomationName = $"Remove rule {r.Pattern}",
715+
RemoveRuleAutomationId = $"RemoveExecPolicyRuleButton_{r.Index}",
714716
ActionBrush = r.Action == "allow"
715717
? allowBrush
716718
: r.Action == "prompt" ? askBrush : denyBrush

src/OpenClaw.WinNode.Cli/Program.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text;
55
using System.Text.Json;
66
using System.Text.RegularExpressions;
7+
using OpenClaw.Shared;
78
using OpenClaw.Shared.Mcp;
89

910
namespace OpenClaw.WinNode.Cli;
@@ -19,6 +20,7 @@ internal sealed class WinNodeOptions
1920
public string? McpUrlOverride { get; set; }
2021
public int? McpPortOverride { get; set; }
2122
public string? McpTokenOverride { get; set; }
23+
public string? Identity { get; set; }
2224
public bool Verbose { get; set; }
2325
}
2426

@@ -151,6 +153,16 @@ public static async Task<int> RunAsync(
151153
return 2;
152154
}
153155

156+
try
157+
{
158+
options.Identity = OpenClawAppIdentity.ResolveIdentity(envLookup, options.Identity);
159+
}
160+
catch (ArgumentException ex)
161+
{
162+
stderr.WriteLine($"Argument error: {ex.Message}");
163+
return 2;
164+
}
165+
154166
var token = ResolveAuthToken(options, envLookup, stderr);
155167
if (token.Source == "error")
156168
{
@@ -533,7 +545,9 @@ private static int ResolveEnvPort(Func<string, string?> envLookup, bool verbose,
533545
/// per-tool secret env-var convention — same shape as <c>GITHUB_TOKEN</c>,
534546
/// <c>ANTHROPIC_API_KEY</c>, <c>NUGET_API_KEY</c>.</item>
535547
/// <item>The on-disk token file the tray writes when MCP is enabled —
536-
/// <c>%APPDATA%\OpenClawTray\mcp-token.txt</c> by default, or
548+
/// <c>%APPDATA%\OpenClawTray\mcp-token.txt</c> by default,
549+
/// <c>%APPDATA%\OpenClawTray-Dev\mcp-token.txt</c> when
550+
/// <c>--identity dev</c> or <c>OPENCLAW_APP_IDENTITY=dev</c> is set, or
537551
/// <c>$OPENCLAW_TRAY_DATA_DIR\mcp-token.txt</c> when the tray was launched
538552
/// with that sandbox override (the integration test fixture uses it).</item>
539553
/// </list>
@@ -559,7 +573,7 @@ internal static AuthTokenResult ResolveAuthToken(
559573
return new AuthTokenResult(envToken, "OPENCLAW_MCP_TOKEN");
560574
}
561575

562-
var path = ResolveTokenPath(envLookup);
576+
var path = ResolveTokenPath(envLookup, options.Identity);
563577

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

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

717725
/// <summary>
@@ -774,6 +782,9 @@ internal static WinNodeOptions ParseArgs(string[] args)
774782
case "--mcp-token":
775783
options.McpTokenOverride = RequireValue(args, ref i, arg);
776784
break;
785+
case "--identity":
786+
options.Identity = OpenClawAppIdentity.NormalizeIdentity(RequireValue(args, ref i, arg));
787+
break;
777788
case "--verbose":
778789
options.Verbose = true;
779790
break;
@@ -830,6 +841,8 @@ internal static void PrintUsage(TextWriter stdout)
830841
stdout.WriteLine(" --mcp-token <token> Bearer token (testing/explicit overrides only - visible to");
831842
stdout.WriteLine(" other processes via the OS process listing). Prefer");
832843
stdout.WriteLine(" $OPENCLAW_MCP_TOKEN or %APPDATA%\\OpenClawTray\\mcp-token.txt");
844+
stdout.WriteLine(" --identity <release|dev> Select tray profile for default token lookup");
845+
stdout.WriteLine(" (default: $OPENCLAW_APP_IDENTITY or release)");
833846
stdout.WriteLine(" --verbose Print endpoint + ignored flags to stderr");
834847
stdout.WriteLine(" --help, -h Show this help");
835848
stdout.WriteLine();

src/OpenClaw.WinNode.Cli/skill.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ argument shape, and the A2UI v0.8 JSONL grammar. It is shipped alongside
2323
## Invocation shape
2424

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

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

0 commit comments

Comments
 (0)