fix: diagnose blocked browser proxy policy

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Scott Hanselman 2026-04-27 02:27:52 -07:00
parent 48387bd1bd
commit 17f377e04c
5 changed files with 75 additions and 1 deletions

View File

@ -435,6 +435,7 @@ Deliverables:
- Dangerous command opt-in guidance: **implemented copyable safety guidance for camera/screen privacy-sensitive commands without emitting one-click dangerous repair commands**
- Node capability settings: **implemented Settings toggles for canvas, screen, camera, location, and browser proxy command groups so privacy-sensitive surfaces can be disabled before reconnecting/re-pairing**
- Disabled capability diagnostics: **implemented Command Center distinction between intentionally disabled Settings groups and true gateway allowlist/parity gaps**
- Browser proxy policy diagnostics: **implemented a specific Command Center warning/copy action for declared `browser.proxy` commands filtered by gateway policy, instead of burying them under generic blocked-command output**
Risk: high for exec/security. Do not rush.

View File

@ -845,10 +845,12 @@ public class NodeCapabilityHealthInfo
public Dictionary<string, bool> Permissions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<string> SafeDeclaredCommands { get; set; } = new();
public List<string> DangerousDeclaredCommands { get; set; } = new();
public List<string> BrowserDeclaredCommands { get; set; } = new();
public List<string> WindowsSpecificDeclaredCommands { get; set; } = new();
public List<string> BlockedDeclaredCommands { get; set; } = new();
public List<string> MissingSafeAllowlistCommands { get; set; } = new();
public List<string> MissingDangerousAllowlistCommands { get; set; } = new();
public List<string> MissingBrowserAllowlistCommands { get; set; } = new();
public List<string> MissingMacParityCommands { get; set; } = new();
public List<string> DisabledBySettingsCommands { get; set; } = new();
public List<GatewayDiagnosticWarning> Warnings { get; set; } = new();
@ -879,6 +881,9 @@ public class NodeCapabilityHealthInfo
DangerousDeclaredCommands = CommandCenterCommandGroups.DangerousCommands
.Where(commandSet.Contains)
.ToList(),
BrowserDeclaredCommands = CommandCenterCommandGroups.BrowserCommands
.Where(commandSet.Contains)
.ToList(),
WindowsSpecificDeclaredCommands = CommandCenterCommandGroups.WindowsSpecificCommands
.Where(commandSet.Contains)
.ToList()
@ -894,6 +899,8 @@ public class NodeCapabilityHealthInfo
info.MissingSafeAllowlistCommands.Add(command);
else if (CommandCenterCommandGroups.DangerousCommandSet.Contains(command))
info.MissingDangerousAllowlistCommands.Add(command);
else if (CommandCenterCommandGroups.BrowserCommandSet.Contains(command))
info.MissingBrowserAllowlistCommands.Add(command);
}
if (isWindows)
@ -991,6 +998,14 @@ public static class CommandCenterCommandGroups
public static readonly FrozenSet<string> DangerousCommandSet =
DangerousCommands.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
public static readonly string[] BrowserCommands =
[
"browser.proxy"
];
public static readonly FrozenSet<string> BrowserCommandSet =
BrowserCommands.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
public static readonly string[] WindowsSpecificCommands =
[
"system.execApprovals.get",
@ -1192,6 +1207,20 @@ public static class CommandCenterDiagnostics
});
}
if (node.MissingBrowserAllowlistCommands.Count > 0)
{
var blocked = string.Join(", ", node.MissingBrowserAllowlistCommands);
warnings.Add(new GatewayDiagnosticWarning
{
Severity = GatewayDiagnosticSeverity.Warning,
Category = "allowlist",
Title = "Browser proxy command is filtered by gateway policy",
Detail = $"{blocked} {(node.MissingBrowserAllowlistCommands.Count == 1 ? "is" : "are")} declared by the node but not allowed by gateway policy. Add the exact browser command and re-approve or re-pair the node if the gateway keeps an older command snapshot.",
RepairAction = "Copy browser proxy allowlist repair command",
CopyText = BuildAllowCommandsRepairCommand(node.MissingBrowserAllowlistCommands)
});
}
if (node.DisabledBySettingsCommands.Count > 0)
{
warnings.Add(new GatewayDiagnosticWarning
@ -1205,7 +1234,8 @@ public static class CommandCenterDiagnostics
if (node.BlockedDeclaredCommands.Count > 0 &&
node.MissingSafeAllowlistCommands.Count == 0 &&
node.MissingDangerousAllowlistCommands.Count == 0)
node.MissingDangerousAllowlistCommands.Count == 0 &&
node.MissingBrowserAllowlistCommands.Count == 0)
{
warnings.Add(new GatewayDiagnosticWarning
{

View File

@ -693,11 +693,13 @@ public sealed partial class StatusDetailWindow : WindowEx
builder.AppendLine($" declared commands: {FormatCommandList(node.Commands)}");
builder.AppendLine($" safe companion commands: {FormatCommandList(node.SafeDeclaredCommands)}");
builder.AppendLine($" privacy-sensitive opt-ins: {FormatCommandList(node.DangerousDeclaredCommands)}");
builder.AppendLine($" browser proxy commands: {FormatCommandList(node.BrowserDeclaredCommands)}");
builder.AppendLine($" Windows-specific commands: {FormatCommandList(node.WindowsSpecificDeclaredCommands)}");
builder.AppendLine($" filtered by gateway policy: {FormatCommandList(node.BlockedDeclaredCommands)}");
builder.AppendLine($" disabled in Settings: {FormatCommandList(node.DisabledBySettingsCommands)}");
builder.AppendLine($" missing safe allowlist: {FormatCommandList(node.MissingSafeAllowlistCommands)}");
builder.AppendLine($" missing privacy-sensitive allowlist: {FormatCommandList(node.MissingDangerousAllowlistCommands)}");
builder.AppendLine($" missing browser proxy allowlist: {FormatCommandList(node.MissingBrowserAllowlistCommands)}");
builder.AppendLine($" missing Mac parity: {FormatCommandList(node.MissingMacParityCommands)}");
}
@ -842,6 +844,8 @@ public sealed partial class StatusDetailWindow : WindowEx
parts.Add($"{node.SafeDeclaredCommands.Count} safe");
if (node.DangerousDeclaredCommands.Count > 0)
parts.Add($"{node.DangerousDeclaredCommands.Count} opt-in");
if (node.BrowserDeclaredCommands.Count > 0)
parts.Add("browser.proxy");
if (node.WindowsSpecificDeclaredCommands.Count > 0)
parts.Add($"{node.WindowsSpecificDeclaredCommands.Count} Windows");
if (node.DisabledBySettingsCommands.Count > 0)
@ -865,8 +869,10 @@ public sealed partial class StatusDetailWindow : WindowEx
builder.AppendLine(BuildNodeSummary(node).TrimEnd());
builder.AppendLine($"Safe companion commands: {FormatCommandList(node.SafeDeclaredCommands)}");
builder.AppendLine($"Privacy-sensitive commands: {FormatCommandList(node.DangerousDeclaredCommands)}");
builder.AppendLine($"Browser proxy commands: {FormatCommandList(node.BrowserDeclaredCommands)}");
builder.AppendLine($"Windows-specific commands: {FormatCommandList(node.WindowsSpecificDeclaredCommands)}");
builder.AppendLine($"Filtered by gateway policy: {FormatCommandList(node.BlockedDeclaredCommands)}");
builder.AppendLine($"Missing browser proxy allowlist: {FormatCommandList(node.MissingBrowserAllowlistCommands)}");
builder.AppendLine($"Disabled in Settings: {FormatCommandList(node.DisabledBySettingsCommands)}");
builder.AppendLine($"Missing Mac parity: {FormatCommandList(node.MissingMacParityCommands)}");
builder.AppendLine();

View File

@ -918,6 +918,7 @@ public class CommandCenterModelTests
Assert.Contains("device.info", CommandCenterCommandGroups.SafeCompanionCommands);
Assert.Contains("device.status", CommandCenterCommandGroups.SafeCompanionCommands);
Assert.Contains("screen.record", CommandCenterCommandGroups.DangerousCommands);
Assert.Contains("browser.proxy", CommandCenterCommandGroups.BrowserCommands);
Assert.Contains("browser.proxy", CommandCenterCommandGroups.MacNodeParityCommands);
}
@ -1075,6 +1076,40 @@ public class CommandCenterModelTests
!string.IsNullOrWhiteSpace(w.CopyText));
}
[Fact]
public void NodeCapabilityHealthInfo_WarnsSpecificallyForBlockedBrowserProxy()
{
var node = new GatewayNodeInfo
{
NodeId = "node-1",
DisplayName = "Windows Node",
Platform = "windows",
IsOnline = true,
Commands =
[
"system.notify",
"system.run",
"system.which",
"browser.proxy"
],
Permissions = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase)
{
["browser.proxy"] = false
}
};
var info = NodeCapabilityHealthInfo.FromNode(node);
Assert.Contains("browser.proxy", info.BrowserDeclaredCommands);
Assert.Contains("browser.proxy", info.MissingBrowserAllowlistCommands);
Assert.DoesNotContain("browser.proxy", info.MissingMacParityCommands);
Assert.Contains(info.Warnings, w =>
w.Title == "Browser proxy command is filtered by gateway policy" &&
w.RepairAction == "Copy browser proxy allowlist repair command" &&
w.CopyText == "openclaw config set gateway.nodes.allowCommands '[\"browser.proxy\"]'");
Assert.DoesNotContain(info.Warnings, w => w.Title == "Some node commands are filtered");
}
[Fact]
public void NodeCapabilityHealthInfo_TreatsDisabledCommandsAsSettingsChoice()
{

View File

@ -176,6 +176,8 @@ public class TrayMenuWindowMarkupTests
Assert.Contains("OpenClaw node inventory", source);
Assert.Contains("Safe companion commands", source);
Assert.Contains("Privacy-sensitive commands", source);
Assert.Contains("Browser proxy commands", source);
Assert.Contains("Missing browser proxy allowlist", source);
Assert.Contains("Disabled in Settings", source);
Assert.Contains("Missing Mac parity", source);
}