Skip to content
Closed
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
25 changes: 11 additions & 14 deletions src/OpenClaw.Shared/Mxc/MxcCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,31 @@ public async Task<CommandResult> RunAsync(CommandRequest request, CancellationTo
{
var settings = _settingsProvider();

// Fail-closed when MXC is unavailable. We do NOT route to host even if the
// persisted toggle is OFF — the UI hides the toggle in that state so any
// OFF value is stale (e.g., flipped on a previous run / different machine).
// The UI's "Sandbox unavailable — commands blocked" claim must match
// actual behavior or it's a lie.
// Explicit user opt-out always routes through the host runner.
if (!settings.SystemRunSandboxEnabled)
{
_logger.Info("[mxc] sandbox=disabled; routing system.run through host runner");
return await _hostFallback.RunAsync(request, ct);
}

// Fail-closed when sandboxing is enabled but MXC is unavailable.
if (!_isSandboxAvailable())
{
_logger.Warn(
"[mxc] system.run DENIED: sandbox unavailable. " +
"[mxc] system.run DENIED: sandbox enabled but unavailable. " +
"Update Windows or install missing components to enable.");
return new CommandResult
{
Stdout = string.Empty,
Stderr =
"Sandboxing is unavailable on this machine, so agent-started Windows " +
"commands are blocked. Open the Sandbox page for fix instructions.",
"Sandboxing is enabled but unavailable on this machine, so agent-started Windows " +
"commands are blocked. Open the Sandbox page to turn off Node Sandbox or for fix instructions.",
ExitCode = -1,
TimedOut = false,
DurationMs = 0,
};
}

if (!settings.SystemRunSandboxEnabled)
{
_logger.Info("[mxc] sandbox=disabled; routing system.run through host runner");
return await _hostFallback.RunAsync(request, ct);
}

var settingsDirectoryPath = _settingsDirectoryPathProvider();
var policy = MxcPolicyBuilder.ForSystemRun(settings, settingsDirectoryPath);
var argsJson = SerializeArgs(request);
Expand Down
27 changes: 18 additions & 9 deletions src/OpenClaw.Tray.WinUI/Pages/SandboxPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,10 @@ private void LoadState()
/// MXC availability AND the current sandbox toggle state. Three visual states:
/// 1. Available + ON → 🛡 "Sandbox is on" + toggle visible
/// 2. Available + OFF → ⚠ "Sandbox is off — high risk" + toggle visible
/// 3. Unavailable → ⚠ "Sandbox unavailable — commands blocked" + toggle hidden
/// When MXC is unavailable the toggle is hidden AND the runner fails closed
/// (MxcCommandRunner.RunAsync short-circuits to a deny response). The user
/// cannot opt out of the block on an unsupported machine — they must fix MXC.
/// 3. Unavailable + ON → ⚠ "Sandbox unavailable — commands blocked" + toggle visible
/// 4. Unavailable + OFF→ ⚠ "Sandbox unavailable — unprotected mode" + toggle visible
/// When MXC is unavailable and sandbox remains ON, commands fail closed.
/// Users on unsupported machines can still turn sandbox OFF to run unprotected.
/// </summary>
private void UpdateSandboxStatusCard()
{
Expand All @@ -158,10 +158,18 @@ private void UpdateSandboxStatusCard()

if (!available)
{
SandboxEnabledToggle.Visibility = Visibility.Visible;
SandboxStatusIcon.Text = "⚠";
SandboxStatusTitle.Text = "Node Sandbox unavailable — commands blocked";
SandboxStatusSubtext.Text = "Containment isn't available on this PC.";
SandboxEnabledToggle.Visibility = Visibility.Collapsed;
if (enabled)
{
SandboxStatusTitle.Text = "Node Sandbox unavailable — commands blocked";
SandboxStatusSubtext.Text = "Containment isn't available on this PC. Turn off Node Sandbox to run commands unprotected.";
}
else
{
SandboxStatusTitle.Text = "Node Sandbox unavailable — unprotected mode";
SandboxStatusSubtext.Text = "Containment isn't available on this PC. Commands run directly on the host.";
}
return;
}

Expand Down Expand Up @@ -213,7 +221,8 @@ private void UpdateUnavailableActionBar(OpenClaw.Shared.Mxc.MxcAvailability avai
UnavailableActionBar.Title = "Your Windows version doesn't support sandboxing yet";
UnavailableActionMessage.Text =
$"{reasonText}\n\nMXC sandboxing requires a recent Windows build with the AppContainer primitives shipped. " +
"Install the latest Windows updates (or join the Windows Insider Program for the newest builds).";
"Install the latest Windows updates (or join the Windows Insider Program for the newest builds). " +
"You can temporarily turn off Node Sandbox above to run commands unprotected.";
UnavailablePrimaryButton.Content = "Open Windows Update";
UnavailablePrimaryButton.Tag = "windowsupdate";
UnavailablePrimaryButton.Visibility = Visibility.Visible;
Expand All @@ -224,7 +233,7 @@ private void UpdateUnavailableActionBar(OpenClaw.Shared.Mxc.MxcAvailability avai
UnavailableActionMessage.Text =
$"{reasonText}\n\nThe MXC bridge script or the wxc-exec binary couldn't be located. " +
"If this is a developer build, run `npm ci` at the repository root. " +
"Otherwise reinstall the companion app.";
"Otherwise reinstall the companion app. You can temporarily turn off Node Sandbox above to run commands unprotected.";
UnavailablePrimaryButton.Content = "Show install instructions";
UnavailablePrimaryButton.Tag = "install";
UnavailablePrimaryButton.Visibility = Visibility.Visible;
Expand Down
17 changes: 7 additions & 10 deletions tests/OpenClaw.Shared.Tests/Mxc/MxcCommandRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,10 @@ public async Task RunAsync_SandboxDisabled_AlwaysRoutesToHost()
}

[Fact]
public async Task RunAsync_MxcUnavailable_BlocksEvenWithSandboxToggleOff()
public async Task RunAsync_MxcUnavailable_RoutesToHostWhenSandboxToggleOff()
{
// The UI hides the toggle when MXC is unavailable. A persisted toggle=OFF
// (from a previous run or different machine) must NOT cause the runner to
// silently route to host — the page says "commands blocked" and the
// runner must match that promise.
// User opted out of sandboxing. Even if MXC is unavailable, we should
// route through host execution.
var executor = new FakeSandboxExecutor();
var fallback = new FakeCommandRunner
{
Expand All @@ -87,12 +85,11 @@ public async Task RunAsync_MxcUnavailable_BlocksEvenWithSandboxToggleOff()

var result = await runner.RunAsync(new CommandRequest { Command = "echo hi" });

Assert.Equal(-1, result.ExitCode);
Assert.Contains("unavailable", result.Stderr, StringComparison.OrdinalIgnoreCase);
Assert.Contains("blocked", result.Stderr, StringComparison.OrdinalIgnoreCase);
// Neither the sandbox executor nor the host fallback should have run.
Assert.Equal(0, result.ExitCode);
Assert.Equal("host", result.Stdout);
// Sandbox executor should not have run.
Assert.Null(executor.LastRequest);
Assert.Null(fallback.LastRequest);
Assert.NotNull(fallback.LastRequest);
}

[Fact]
Expand Down