Skip to content
Merged
15 changes: 15 additions & 0 deletions docs/WINDOWS_NODE_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ owns the package upgrade and removal gates.

Every new Windows node call must be exposed through local MCP and `winnode`: register the capability, update `McpToolBridge.CommandDescriptions`, update `.agents/skills/winnode/SKILL.md`, add focused tests, and prove discovery/invocation with `winnode` or raw MCP JSON-RPC.

### Permissions exec policy editor

The Permissions page shows the `main`, wildcard (`*`), then defaults cascade
and lists both wildcard and main allowlist entries. Removing a displayed entry
edits its original bucket, not every matching pattern. A duplicate in the other
bucket remains effective until it is removed separately. Adding a rule writes
to `main` and preserves inherited wildcard security and ask settings, except
for the existing transition from effective Deny to a main allowlist.

Use an isolated profile for visual proof: seed distinguishable wildcard and
main rules, remove the wildcard row, then reopen the page and confirm only
that entry is gone from its original bucket. Also check equal patterns in both
buckets and adding a main rule while inheriting wildcard Full. These editor
checks do not require executing a command and are not MXC containment proof.

### 1. Settings Toggle
- Verify the toggle appears in Settings under "ADVANCED"
- Verify it saves and persists across app restarts
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Pattern}"
<TextBlock Grid.Column="0" Text="{Binding DisplayText}"
FontFamily="Consolas" FontSize="13"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Expand Down
32 changes: 24 additions & 8 deletions src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,30 @@ private void UpdateExecApprovals()
SelectComboBoxTag(DefaultActionCombo, _viewModel.DefaultExecActionTag);
_suppressDefaultActionChange = false;

PolicyRulesList.ItemsSource = _viewModel.ExecApprovalRules.Select((rule, index) => new
{
Rule = rule,
rule.Pattern,
RemoveRuleAutomationName = $"Remove allowlist entry {rule.Pattern}",
RemoveRuleAutomationId = $"RemoveExecPolicyRuleButton_{index}",
Action = "allow",
ActionBrush = (Brush)Application.Current.Resources["SystemFillColorSuccessBrush"],
var inactiveAction = LocalizationHelper.GetString("PermissionsPage_ExecRuleAction_Inactive");
var action = _viewModel.ExecApprovalRulesActive ? "allow" : inactiveAction;
var actionBrushKey = _viewModel.ExecApprovalRulesActive
? "SystemFillColorSuccessBrush"
: "SystemFillColorNeutralBrush";
PolicyRulesList.ItemsSource = _viewModel.ExecApprovalRules.Select((rule, index) =>
{
var scopeLabel = LocalizationHelper.GetString(
rule.IsWildcard
? "PermissionsPage_ExecRuleScope_Wildcard"
: "PermissionsPage_ExecRuleScope_Main");
return new
{
Rule = rule,
rule.Pattern,
DisplayText = $"{rule.Pattern} ({scopeLabel})",
RemoveRuleAutomationName = LocalizationHelper.Format(
"PermissionsPage_RemoveRuleAutomationNameFormat",
scopeLabel,
rule.Pattern),
RemoveRuleAutomationId = $"RemoveExecPolicyRuleButton_{index}",
Action = action,
ActionBrush = (Brush)Application.Current.Resources[actionBrushKey],
};
}).ToList();

var count = _viewModel.ExecApprovalRules.Count;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ internal sealed record PermissionsExecApprovalRule(
string Pattern,
string? ArgPattern,
double? LastUsedAt,
string? LastResolvedPath);
string? LastResolvedPath,
bool IsWildcard);

internal sealed record PermissionsRuntimeSourceSnapshot(
GatewayConnectionSnapshot ConnectionSnapshot,
Expand Down
108 changes: 75 additions & 33 deletions src/OpenClaw.Tray.WinUI/Presentation/PermissionsPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal sealed class PermissionsPageViewModel : INavigationAware, IDisposable,
private PermissionsGatewayAllowlistState _gatewayAllowlistState;
private string _defaultExecActionTag = "deny";
private IReadOnlyList<PermissionsExecApprovalRule> _execApprovalRules = Array.Empty<PermissionsExecApprovalRule>();
private bool _execApprovalRulesActive;
private ExecApprovalsSnapshotFailure? _execApprovalsFailure;
private PermissionsExecApprovalsStatus _execApprovalsStatus;
private long _execApprovalsStatusVersion;
Expand Down Expand Up @@ -136,6 +137,7 @@ public bool McpEnabled
public string DefaultExecActionTag => _defaultExecActionTag;

public IReadOnlyList<PermissionsExecApprovalRule> ExecApprovalRules => _execApprovalRules;
public bool ExecApprovalRulesActive => _execApprovalRulesActive;
public ExecApprovalsSnapshotFailure? ExecApprovalsFailure => _execApprovalsFailure;
public PermissionsExecApprovalsStatus ExecApprovalsStatus => _execApprovalsStatus;
public long ExecApprovalsStatusVersion => _execApprovalsStatusVersion;
Expand Down Expand Up @@ -244,7 +246,8 @@ public Task<bool> RemoveExecApprovalRuleAsync(PermissionsExecApprovalRule rule)
null,
rule.Pattern,
rule.Id,
rule.ArgPattern));
rule.ArgPattern,
rule.IsWildcard));
}

private void PersistSetting(Action<ISettingsEditor> edit)
Expand Down Expand Up @@ -623,6 +626,22 @@ private void CompleteExecApprovalsMutation(ExecApprovalsSnapshot updated, long m

private void ApplyMutation(ExecApprovalsFile file, ExecApprovalsMutation mutation)
{
if (mutation.Kind == ExecApprovalsMutationKind.RemoveRule)
{
if (file.Agents is not null
&& file.Agents.TryGetValue(mutation.IsWildcard ? "*" : "main", out var agent))
{
agent?.Allowlist?.RemoveAll(entry =>
mutation.RuleId.HasValue
? entry.Id == mutation.RuleId
: entry.Id is null
&& string.Equals(entry.Pattern?.Trim(), mutation.Pattern, StringComparison.OrdinalIgnoreCase)
&& string.Equals(entry.ArgPattern, mutation.ArgPattern, StringComparison.Ordinal));
}

return;
}

file.Version = 1;
file.Defaults ??= new ExecApprovalsDefaults();
file.Agents ??= new Dictionary<string, ExecApprovalsAgent>(StringComparer.Ordinal);
Expand Down Expand Up @@ -654,10 +673,15 @@ private void ApplyMutation(ExecApprovalsFile file, ExecApprovalsMutation mutatio
var allowlist = main.Allowlist ??= new List<ExecAllowlistEntry>();
if (mutation.Kind == ExecApprovalsMutationKind.AddRule)
{
if ((main.Security ?? file.Defaults.Security ?? ExecSecurity.Deny) == ExecSecurity.Deny)
var displayed = ResolveDisplayedExecPolicy(file);
var inheritedDenyWithDormantRules = main.Security is null
&& displayed.Security == ExecSecurity.Deny
&& displayed.Ask != ExecAsk.Always
&& displayed.Allowlist.Count > 0;
if (displayed.Security == ExecSecurity.Deny && !inheritedDenyWithDormantRules)
{
main.Security = ExecSecurity.Allowlist;
main.Ask = ExecAsk.Off;
main.Ask = displayed.Ask;
main.AskFallback = ExecSecurity.Deny;
}

Expand All @@ -675,13 +699,6 @@ private void ApplyMutation(ExecApprovalsFile file, ExecApprovalsMutation mutatio

return;
}

allowlist.RemoveAll(entry =>
mutation.RuleId.HasValue
? entry.Id == mutation.RuleId
: entry.Id is null
&& string.Equals(entry.Pattern?.Trim(), mutation.Pattern, StringComparison.OrdinalIgnoreCase)
&& string.Equals(entry.ArgPattern, mutation.ArgPattern, StringComparison.Ordinal));
}

private void LoadExecApprovals(ExecApprovalsReadOnlySnapshotResult result)
Expand All @@ -701,26 +718,29 @@ private void LoadExecApprovals(ExecApprovalsReadOnlySnapshotResult result)
_execApprovalsBaseHash = result.Failure?.Hash;
SetField(ref _defaultExecActionTag, "deny", nameof(DefaultExecActionTag));
SetField(ref _execApprovalRules, Array.Empty<PermissionsExecApprovalRule>(), nameof(ExecApprovalRules));
SetField(ref _execApprovalRulesActive, false, nameof(ExecApprovalRulesActive));
SetExecApprovalsFailure(result.Failure, PermissionsExecApprovalsStatus.None, bumpVersion: false);
}
}

private void ApplyExecSnapshot(ExecApprovalsSnapshot snapshot)
{
_execApprovalsBaseHash = snapshot.Hash;
SetField(ref _defaultExecActionTag, MapDefaultAction(snapshot.File), nameof(DefaultExecActionTag));
var rules = ((IEnumerable<ExecAllowlistEntry>)(snapshot.File.Agents is not null
&& snapshot.File.Agents.TryGetValue("main", out var main)
&& main?.Allowlist is not null
? main.Allowlist
: Array.Empty<ExecAllowlistEntry>()))
.Where(entry => !string.IsNullOrWhiteSpace(entry.Pattern))
.Select(entry => new PermissionsExecApprovalRule(
entry.Id,
entry.Pattern!,
entry.ArgPattern,
entry.LastUsedAt,
entry.LastResolvedPath))
var displayed = ResolveDisplayedExecPolicy(snapshot.File);
SetField(ref _defaultExecActionTag, MapDefaultAction(displayed), nameof(DefaultExecActionTag));
SetField(
ref _execApprovalRulesActive,
displayed.Security != ExecSecurity.Deny,
nameof(ExecApprovalRulesActive));
var rules = displayed.Allowlist
.Where(rule => !string.IsNullOrWhiteSpace(rule.Entry.Pattern))
.Select(rule => new PermissionsExecApprovalRule(
rule.Entry.Id,
rule.Entry.Pattern!,
rule.Entry.ArgPattern,
rule.Entry.LastUsedAt,
rule.Entry.LastResolvedPath,
rule.IsWildcard))
.ToArray();
SetField(ref _execApprovalRules, rules, nameof(ExecApprovalRules));
}
Expand Down Expand Up @@ -758,20 +778,41 @@ private void HandleOnUiThread(Action action)
_ => null,
};

private static string MapDefaultAction(ExecApprovalsFile file)
private readonly record struct DisplayedExecPolicy(
ExecSecurity Security,
ExecAsk Ask,
IReadOnlyList<(ExecAllowlistEntry Entry, bool IsWildcard)> Allowlist);

private static DisplayedExecPolicy ResolveDisplayedExecPolicy(ExecApprovalsFile file)
{
file.Defaults ??= new ExecApprovalsDefaults();
file.Agents ??= new Dictionary<string, ExecApprovalsAgent>(StringComparer.Ordinal);
file.Agents.TryGetValue("main", out var main);
var security = main?.Security ?? file.Defaults.Security ?? ExecSecurity.Deny;
var ask = main?.Ask ?? file.Defaults.Ask ?? ExecAsk.OnMiss;
return security switch
ExecApprovalsAgent? main = null;
ExecApprovalsAgent? wildcard = null;
file.Agents?.TryGetValue("main", out main);
file.Agents?.TryGetValue("*", out wildcard);
var allowlist = new List<(ExecAllowlistEntry Entry, bool IsWildcard)>();
if (wildcard?.Allowlist is not null)
{
allowlist.AddRange(wildcard.Allowlist.Select(entry => (entry, true)));
}

if (main?.Allowlist is not null)
{
allowlist.AddRange(main.Allowlist.Select(entry => (entry, false)));
}

return new DisplayedExecPolicy(
main?.Security ?? wildcard?.Security ?? file.Defaults?.Security ?? ExecSecurity.Allowlist,
main?.Ask ?? wildcard?.Ask ?? file.Defaults?.Ask ?? ExecAsk.OnMiss,
allowlist);
}

private static string MapDefaultAction(DisplayedExecPolicy displayed) =>
displayed.Security switch
{
ExecSecurity.Full => "allow",
ExecSecurity.Allowlist when ask is ExecAsk.OnMiss or ExecAsk.Always => "prompt",
ExecSecurity.Allowlist when displayed.Ask is ExecAsk.OnMiss or ExecAsk.Always => "prompt",
_ => "deny",
};
}

private static string NormalizeAction(string? action)
{
Expand Down Expand Up @@ -860,5 +901,6 @@ private sealed record ExecApprovalsMutation(
string? Action,
string? Pattern,
Guid? RuleId = null,
string? ArgPattern = null);
string? ArgPattern = null,
bool IsWildcard = false);
}
12 changes: 12 additions & 0 deletions src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3510,6 +3510,18 @@ Commands are blocked while sandboxing is unavailable because strict fallback blo
<data name="PermissionsPage_RulesCount_ManyFormat" xml:space="preserve">
<value>{0} entries</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Main" xml:space="preserve">
<value>this agent</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Wildcard" xml:space="preserve">
<value>all agents</value>
</data>
<data name="PermissionsPage_RemoveRuleAutomationNameFormat" xml:space="preserve">
<value>Remove {0} allowlist entry {1}</value>
</data>
<data name="PermissionsPage_ExecRuleAction_Inactive" xml:space="preserve">
<value>inactive</value>
</data>
<data name="PermissionsPage_Allowlist_NoConfig" xml:space="preserve">
<value>No allowlist data available. Connect to a gateway first.</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3463,6 +3463,18 @@ Les commandes sont bloquées tant que le sandboxing est indisponible, car le blo
<data name="PermissionsPage_RulesCount_ManyFormat" xml:space="preserve">
<value>{0} entrées</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Main" xml:space="preserve">
<value>cet agent</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Wildcard" xml:space="preserve">
<value>tous les agents</value>
</data>
<data name="PermissionsPage_RemoveRuleAutomationNameFormat" xml:space="preserve">
<value>Supprimer l’entrée {1} de la liste d’autorisation pour {0}</value>
</data>
<data name="PermissionsPage_ExecRuleAction_Inactive" xml:space="preserve">
<value>désactivée</value>
</data>
<data name="PermissionsPage_Allowlist_NoConfig" xml:space="preserve">
<value>No allowlist data available. Connect to a gateway first.</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3464,6 +3464,18 @@ Opdrachten worden geblokkeerd zolang sandboxing niet beschikbaar is, omdat strik
<data name="PermissionsPage_RulesCount_ManyFormat" xml:space="preserve">
<value>{0} vermeldingen</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Main" xml:space="preserve">
<value>deze agent</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Wildcard" xml:space="preserve">
<value>alle agents</value>
</data>
<data name="PermissionsPage_RemoveRuleAutomationNameFormat" xml:space="preserve">
<value>Vermelding {1} uit de toestaanlijst voor {0} verwijderen</value>
</data>
<data name="PermissionsPage_ExecRuleAction_Inactive" xml:space="preserve">
<value>inactief</value>
</data>
<data name="PermissionsPage_Allowlist_NoConfig" xml:space="preserve">
<value>No allowlist data available. Connect to a gateway first.</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions src/OpenClaw.Tray.WinUI/Strings/pt-br/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3482,6 +3482,18 @@ Os comandos são bloqueados enquanto o sandboxing está indisponível porque o b
<data name="PermissionsPage_RulesCount_ManyFormat" xml:space="preserve">
<value>{0} entradas</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Main" xml:space="preserve">
<value>este agente</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Wildcard" xml:space="preserve">
<value>todos os agentes</value>
</data>
<data name="PermissionsPage_RemoveRuleAutomationNameFormat" xml:space="preserve">
<value>Remover a entrada {1} da lista de permissões de {0}</value>
</data>
<data name="PermissionsPage_ExecRuleAction_Inactive" xml:space="preserve">
<value>inativa</value>
</data>
<data name="PermissionsPage_Allowlist_NoConfig" xml:space="preserve">
<value>No allowlist data available. Connect to a gateway first.</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3463,6 +3463,18 @@
<data name="PermissionsPage_RulesCount_ManyFormat" xml:space="preserve">
<value>{0} 个条目</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Main" xml:space="preserve">
<value>此代理</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Wildcard" xml:space="preserve">
<value>所有代理</value>
</data>
<data name="PermissionsPage_RemoveRuleAutomationNameFormat" xml:space="preserve">
<value>移除{0}的允许列表条目 {1}</value>
</data>
<data name="PermissionsPage_ExecRuleAction_Inactive" xml:space="preserve">
<value>未启用</value>
</data>
<data name="PermissionsPage_Allowlist_NoConfig" xml:space="preserve">
<value>No allowlist data available. Connect to a gateway first.</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3463,6 +3463,18 @@
<data name="PermissionsPage_RulesCount_ManyFormat" xml:space="preserve">
<value>{0} 個項目</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Main" xml:space="preserve">
<value>此代理</value>
</data>
<data name="PermissionsPage_ExecRuleScope_Wildcard" xml:space="preserve">
<value>所有代理</value>
</data>
<data name="PermissionsPage_RemoveRuleAutomationNameFormat" xml:space="preserve">
<value>移除{0}的允許清單項目 {1}</value>
</data>
<data name="PermissionsPage_ExecRuleAction_Inactive" xml:space="preserve">
<value>未啟用</value>
</data>
<data name="PermissionsPage_Allowlist_NoConfig" xml:space="preserve">
<value>No allowlist data available. Connect to a gateway first.</value>
</data>
Expand Down
7 changes: 6 additions & 1 deletion tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,12 @@ public void PermissionsPage_ExecPolicyRemoveButtons_HaveAccessibleNames()

Assert.Contains("AutomationProperties.Name=\"{Binding RemoveRuleAutomationName}\"", xaml);
Assert.Contains("AutomationProperties.AutomationId=\"{Binding RemoveRuleAutomationId}\"", xaml);
Assert.Contains("RemoveRuleAutomationName = $\"Remove allowlist entry {rule.Pattern}\"", codeBehind);
Assert.Contains("\"PermissionsPage_ExecRuleScope_Wildcard\"", codeBehind);
Assert.Contains("\"PermissionsPage_ExecRuleScope_Main\"", codeBehind);
Assert.Contains("DisplayText = $\"{rule.Pattern} ({scopeLabel})\"", codeBehind);
Assert.Contains("\"PermissionsPage_RemoveRuleAutomationNameFormat\"", codeBehind);
Assert.Contains("\"PermissionsPage_ExecRuleAction_Inactive\"", codeBehind);
Assert.Contains("\"SystemFillColorNeutralBrush\"", codeBehind);
Assert.Contains("RemoveRuleAutomationId = $\"RemoveExecPolicyRuleButton_{index}\"", codeBehind);
}

Expand Down
Loading
Loading