Sabbour.Mxc.Sdk 0.1.2

dotnet add package Sabbour.Mxc.Sdk --version 0.1.2
                    
NuGet\Install-Package Sabbour.Mxc.Sdk -Version 0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sabbour.Mxc.Sdk" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sabbour.Mxc.Sdk" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Sabbour.Mxc.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sabbour.Mxc.Sdk --version 0.1.2
                    
#r "nuget: Sabbour.Mxc.Sdk, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sabbour.Mxc.Sdk@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sabbour.Mxc.Sdk&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=Sabbour.Mxc.Sdk&version=0.1.2
                    
Install as a Cake Tool

Sabbour.Mxc.Sdk — unofficial, experimental .NET SDK for MXC (Microsoft eXecution Containers)

CI NuGet NuGet downloads License: MIT .NET

A .NET 10 SDK for MXC (Microsoft eXecution Containers) — a faithful port of the TypeScript @microsoft/mxc-sdk package that brings the same policy-driven sandboxing to .NET applications. MXC runs a command inside OS-level isolation governed by a single policy: it decides which filesystem paths the process can read or write and whether it can reach the network, and enforces those limits at the kernel boundary.

This is experimental code. APIs, behavior, and packaging may change without notice, and it is not supported for production use. The underlying MXC executor is itself under active development.

Policy enforcement in action

One small probe does two things a workload usually should not: it reaches the network, then reads an SSH private key that lives outside its workspace. The SDK runs that same probe twice through MxcSdk.SpawnSandboxAsync, changing nothing but the SandboxPolicy:

// probe.sh, run unchanged under both policies:
//   curl https://api.github.com/zen     # reach the network
//   cat  ~/.ssh/id_ed25519              # read a credential outside the workspace
string command = "sh probe.sh";

// WITHOUT restrictions: outbound allowed, the credential's folder is readable.
var permissive = new SandboxPolicy
{
    Version = "0.6.0-alpha",
    Network = new NetworkPolicy { AllowOutbound = true },
    Filesystem = new FilesystemPolicy { ReadwritePaths = [root] },
};

// WITH policy: no outbound, only the workspace is exposed.
var restrictive = new SandboxPolicy
{
    Version = "0.6.0-alpha",
    Network = new NetworkPolicy { AllowOutbound = false },
    Filesystem = new FilesystemPolicy { ReadwritePaths = [workspace] },
};

// Same command, same call — only the policy changes.
foreach (var policy in new[] { permissive, restrictive })
{
    var result = await MxcSdk.SpawnSandboxAsync(command, policy);
    Console.WriteLine(result.Stdout);
}

Running it prints the two outcomes side by side:

$ dotnet run --project examples/10-policy-enforcement -c Release

credential:  /tmp/mxc-policy-demo/home/.ssh/id_ed25519 (SSH private key, outside the workspace)
workspace:   /tmp/mxc-policy-demo/workspace

=== WITHOUT restrictions  (allowOutbound=true, credential folder readable) ===
[network]    curl https://api.github.com/zen
Non-blocking is better than blocking.

[filesystem] cat /tmp/mxc-policy-demo/home/.ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
ThisIsAFakeDemoKeyThatExistsOnlyToBeBlockedByPolicyDoNotUseItAnywhere==
-----END OPENSSH PRIVATE KEY-----

=== WITH policy           (allowOutbound=false, only workspace readable) ===
[network]    curl https://api.github.com/zen
curl: (6) Could not resolve host: api.github.com

[filesystem] cat /tmp/mxc-policy-demo/home/.ssh/id_ed25519
cat: /tmp/mxc-policy-demo/home/.ssh/id_ed25519: No such file or directory

Install

The package is published on NuGet.org: Sabbour.Mxc.Sdk.

dotnet add package Sabbour.Mxc.Sdk

Or add a PackageReference to your .csproj:

<PackageReference Include="Sabbour.Mxc.Sdk" Version="0.1.1" />

Enabling isolation backends (host setup)

The SDK picks a containment backend, but the host has to have that backend lit up first. Which backends are available depends on the host OS — Windows and WSL/Linux are covered separately below.

Windows host

The default processcontainer path works out of the box on recent Windows builds; the other backends need one-time setup. The steps below are the ones that get each backend running with the v0.6.1 executor binaries.

Check which tier the executor will select on the current host (read-only, no admin):

$arch = "x64" # use "arm64" on ARM64 hosts
& "$env:MXC_BIN_DIR\$arch\wxc-exec.exe" --probe
processcontainer

processcontainer has three tiers (highest first): base-container, appcontainer-bfs, appcontainer-dacl. The probe reports which one applies.

  • base-container uses an experimental kernel API that ships behind a Windows Feature Store gate on current builds. When the gate is closed, the executor returns E_NOTIMPL even though the API is present. Light it up with ViVeTool (download the build that matches your CPU arch), then reboot:

    # Run elevated. Use comma-separated IDs — repeated /id: flags are rejected.
    .\ViVeTool.exe /enable /id:61389575,61155944
    .\ViVeTool.exe /query /id:61389575
    

    The query command should report the feature as enabled before you retry the executor.

    Older policy schemas (0.4.0-alpha) take the ungated AppContainer path instead, so they run without this gate.

  • appcontainer-dacl needs a one-time host preparation that grants the AppContainer SIDs the ACEs they require. Run wxc-host-prep elevated (it exits non-zero if not):

    $arch = "x64" # use "arm64" on ARM64 hosts
    & "$env:MXC_BIN_DIR\$arch\wxc-host-prep.exe" prepare-system-drive   # one-time, persists
    & "$env:MXC_BIN_DIR\$arch\wxc-host-prep.exe" prepare-null-device    # per-boot
    
windows_sandbox

A real disposable-VM backend (host daemon + in-VM guest). Enable the Windows Sandbox feature elevated, then reboot:

Enable-WindowsOptionalFeature -Online -FeatureName Containers-DisposableClientVM -All

It also needs hardware virtualization enabled in firmware and Python on the host. On Windows builds 26100 and newer there is a documented boot regression (zombie VM processes) that can keep the sandbox VM from starting even when the feature is enabled.

Run the spawning process elevated (as Administrator). Before booting the VM, the executor confirms the feature is on with dism /online /get-featureinfo /featurename:Containers-DisposableClientVM, which requires admin. Without elevation that call fails with Error: 740 (elevated permissions required) and the executor reports Windows Sandbox is not enabled. ... and reboot. even when the feature is enabled — a non-elevated process can't see the feature state. Verified working on Windows 11 arm64 (build 26200): elevated, a cmd /c echo ran to completion inside the VM with exit code 0 in ~30s.

This backend is now implemented and selectable through both CreateConfigFromPolicy and BuildSandboxPayload on Windows (this SDK implements it ahead of upstream — the TypeScript createConfigFromPolicy still throws "not yet supported"). The SDK generates a minimal config (version, containerId, lifecycle, process, containment only — no filesystem/network/ui sections). Spawning still requires Experimental = true:

var config = MxcSdk.BuildSandboxPayload("echo hi", policy, containment: "windows_sandbox");
using var conn = MxcSdk.SpawnSandboxProcessFromConfig(config,
    new SandboxSpawnOptions { Experimental = true, UsePty = false });

For hand-built configs, optional tuning is available via experimental.windows_sandbox with idleTimeoutMs (executor default 300000) and daemonPipeName (executor default "wxc-windows-sandbox"). The policy-based builders do not set these fields — executor defaults apply.

microvm (NanVix)

Requires the nanvixd.exe daemon, which is not included in the public mxc-release-binaries.zip, so it cannot run from the released binaries. This backend also rejects any policy that sets network (no network-policy enforcement).

wslc

wslc runs Linux OCI containers in a dedicated WSL-managed Hyper-V VM. It is still under development and requires WSL 2.8.1 or newer.

hyperlight

hyperlight runs workloads as x86_64 guest code inside a hardware micro-VM (WHP on Windows, KVM on Linux). It requires an x86_64 host — the snapshot tooling has no arm64 guest, so on an arm64 machine --setup-hyperlight exits with requires x86_64 (Hyperlight needs KVM or WHP).

On an x86_64 host, warm the published snapshot once before first use (pulls the kernel + initrd via docker/podman):

& "$env:MXC_BIN_DIR\x64\wxc-exec.exe" --setup-hyperlight

Unlike windows_sandbox (which is selectable through CreateConfigFromPolicy), hyperlight is not — reach it with a prebuilt config and Experimental = true.

WSL / Linux host

On WSL2 / Linux the SDK uses the Linux executor (lxc-exec). Two things light it up — verified on Ubuntu-24.04 (arm64) under WSL2, where the default process containment runs cleanly:

  1. Place the Linux executor where the SDK looks for it. The released mxc-release-binaries.zip ships the Windows wxc-exec.exe; on Linux the SDK resolves lxc-exec from MXC_BIN_DIR/<arch>/ (<arch> is arm64 or x64). Copy the Linux lxc-exec build there and mark it executable:

    mkdir -p "$HOME/mxc-bin/arm64"
    cp ./lxc-exec "$HOME/mxc-bin/arm64/lxc-exec"
    chmod +x "$HOME/mxc-bin/arm64/lxc-exec"
    export MXC_BIN_DIR="$HOME/mxc-bin"
    
  2. Install bubblewrap. The default process containment runs the workload under bwrap. Without it the executor exits with Bubblewrap (bwrap) is not installed or not on PATH:

    sudo apt-get install -y bubblewrap
    

The process, lxc, and bubblewrap containments target this Linux executor. The Windows-only backends (windows_sandbox, microvm, hyperlight) are not reachable from WSL — microvm/hyperlight need an x86_64 host with KVM, which is not exposed inside this WSL2 VM.

Backend availability and limitations

This SDK is a faithful port: it builds the policy/config and hands off to the native MXC executor (wxc-exec on Windows, lxc-exec on Linux). Which backends actually run is decided by the executor and the host OS — not by this SDK. The authoritative requirements live upstream in microsoft/mxc. Here is what we observed while building and testing this port:

Backend Containment What we saw Requirement
processcontainer process (default, Windows) base-container tier returns E_NOTIMPL on a stock build Enable the velocity feature keys 61389575,61155944 with ViVeTool + reboot (see host setup). Not an Insider requirement. Schema 0.4.0-alpha uses the AppContainer tier and runs without those keys.
windows_sandbox vm Runs, but only elevated Windows Sandbox optional feature + reboot, and an elevated process (see host setup). Pass Experimental = true. First boot ~30s.
isolation_session state-aware Unavailable on a stock build Windows Insider build 26300.8553+ (Insider Preview). Only backend that supports the state-aware lifecycle.
lxc / bubblewrap lxc / process (Linux) One-shot only lxc-exec has no state-aware lifecycle — each spawn is a fresh container.
microvm microvm Could not run nanvixd is not included in the published mxc-release-binaries.zip.
hyperlight vm Could not run x86_64-only opt-in build flavor; not in default binaries, unavailable on ARM64.

The state-aware lifecycle is isolation_session-only

The provision → start → exec (many) → stop → deprovision lifecycle (examples/05-state-aware-lifecycle) is implemented only for isolation_session on Windows. On Linux the executor (lxc-exec) is one-shot and rejects a state-aware request with Request error. For every other backend, use the one-shot spawn APIs (CreateConfigFromPolicy + SpawnSandboxFromConfig). See upstream State-Aware Sandboxes.

Other notes

  • Omitting policy.version defaults to the newest schema (0.7.0-alpha), which selects the velocity tier on Windows and fails if the keys above aren't enabled. Pin 0.4.0-alpha for the AppContainer fallback.
  • Network host allow/block lists (network.allowedHosts / network.blockedHosts) are not enforced on Windows — use network.defaultPolicy or network.proxy (upstream).

Verified on Windows 11 ARM64 (Snapdragon X, build 26200) and WSL Ubuntu-24.04 (aarch64), against the v0.6.1 executor binaries. Backend availability and requirements are owned upstream and may change — treat the microsoft/mxc SDK README as the source of truth.

WSL2 sandboxing (Windows host, Linux isolation)

When to use: you are on a Windows host and want real Linux namespace isolation (filesystem confinement, private PID namespace) without a full VM, and WSL2 is available. This path does not go through wxc-exec at all — it invokes wsl.exe directly and runs bwrap or unshare inside the default WSL2 distribution.

Prerequisites

  1. WSL2 installed and a Linux distribution set as default (Ubuntu 24.04 recommended).

  2. bubblewrap available inside WSL2:

    sudo apt install bubblewrap
    

Detection

var wsl2Support = MxcSdk.GetWsl2PlatformSupport();
// wsl2Support.IsSupported     — true when WSL2 is available AND at least one tool found
// wsl2Support.AvailableMethods — subset of { WslBubblewrap, WslUnshare }

Spawning

using Sabbour.Mxc.Sdk;
using Sabbour.Mxc.Sdk.Sandbox;

var wsl2Support = MxcSdk.GetWsl2PlatformSupport();
if (wsl2Support.IsSupported && wsl2Support.AvailableMethods.Contains(ContainmentBackend.WslBubblewrap))
{
    var policy = new SandboxPolicy
    {
        Version = "0.6.0-alpha",
        Filesystem = new FilesystemPolicy { ReadwritePaths = [@"C:\my\workspace"] },
    };

    var result = await MxcSdk.SpawnWsl2SandboxAsync(
        "echo hello from sandbox",
        policy,
        workingDirectory: @"C:\my\workspace");

    Console.WriteLine(result.Stdout);   // hello from sandbox
    Console.WriteLine(result.ExitCode); // 0
}

How it works

  • workingDirectory is mapped from Windows format (C:\...) to the WSL2 mount path (/mnt/c/...).
  • The script is base64-encoded and decoded inside WSL2 via printf %s '<b64>' | base64 -d to prevent shell injection.
  • WslBubblewrap (recommended): confines the filesystem to the workspace directory (bind-mounted read-write), mounts /usr and /etc read-only, creates /bin, /lib, /sbin symlinks (Ubuntu ARM64 layout), and isolates the PID namespace via --unshare-pid. Verified on Ubuntu 24.04 aarch64 WSL2 (bwrap 0.9.0).
  • WslUnshare: user/mount/PID namespace isolation via unshare --user --map-root-user --mount --pid --fork. Does not confine the filesystem to the workspace.
  • Both backends do NOT enforce a network allowlist — outbound network is unrestricted.

Upgrade path to Wslc

When WSL 2.8.1+ ships as a public Windows Update / winget release (currently only tagged on GitHub as of June 2026), this path can be replaced with MxcSdk.SpawnSandboxAsync using ContainmentBackend.Wslc. That path goes fully through wxc-exec using the Wslc SDK (wslcsdk.dll) and provides OCI container isolation without a separate bwrap invocation. Detect availability with:

var support = MxcSdk.GetPlatformSupport();
if (support.AvailableMethods.Contains(ContainmentBackend.Wslc))
{
    // Use SpawnSandboxAsync with containment: "wslc" instead
}

Quickstart

Start with a policy and turn it into the backend config that the native executor understands:

using Sabbour.Mxc.Sdk;

var policy = new SandboxPolicy
{
    Version = "0.6.0-alpha",
    Network = new NetworkPolicy { AllowOutbound = false },
};

ContainerConfig config = MxcSdk.CreateConfigFromPolicy(policy, containment: "process");
Console.WriteLine(config.Containment);

Spawning a sandboxed process uses the same policy, but it also needs the native MXC executor. See Troubleshooting before running spawn examples.

Examples

The examples/ folder has runnable console projects that reference the local SDK source, one per scenario — policy-to-config transforms, platform probing, buffered spawns, filesystem and network policy, the state-aware lifecycle, and the side-by-side policy enforcement demo shown above. See examples/README.md for the full list and which ones need the native executor.

Run any example with:

dotnet run --project examples\01-policy-to-config -c Release

API guide

Policy → ContainerConfig transform

Convert a security-intent policy into a backend-specific container configuration:

ContainerConfig config = MxcSdk.CreateConfigFromPolicy(policy, containment: "process");
// Customize further before spawning:
config = config with
{
    Process = config.Process! with { CommandLine = "python -c \"print('hi')\"" }
};

Spawning

Live PTY (interactive)
// Live PTY spawn (TS spawnSandbox) — async due to Porta.Pty
await using var pty = await MxcSdk.SpawnSandbox(
    script: "python repl.py",
    policy: policy,
    workingDirectory: @"C:\workspace");

pty.DataReceived += chunk =>
    Console.Write(System.Text.Encoding.UTF8.GetString(chunk.Span));
pty.Write("print('hello')\n");
var exit = await pty.WaitForExitAsync();
Buffered one-shot (TS spawnSandboxAsync)
var result = await MxcSdk.SpawnSandboxAsync(
    "node -e \"console.log('done')\"",
    policy);
// result.Stdout, result.Stderr, result.ExitCode
Pipe mode

When PTY overhead is unnecessary (CI, batch jobs), use pipe mode for separate stdout/stderr:

using var conn = MxcSdk.SpawnSandboxProcessFromConfig(config,
    new SandboxSpawnOptions { UsePty = false });
int exitCode = await conn.WaitForExitAsync();
Console.WriteLine(conn.GetStdout());

State-aware isolation session lifecycle

The state-aware API manages sandbox lifecycle phases: provision → start → exec → stop → deprovision.

using Sabbour.Mxc.Sdk;
using Sabbour.Mxc.Sdk.Sandbox;
using Sabbour.Mxc.Sdk.StateAware;

// Containment marker — mirrors TS provisionSandbox(containment, config?, options?)
var backend = IsolationSessionBackend.Instance;

// 1. Provision
var provision = await MxcSdk.ProvisionSandboxAsync(backend,
    new IsolationSessionProvisionConfig { /* backend-specific */ });
var sandboxId = provision.SandboxId;

// 2. Start
await MxcSdk.StartSandboxAsync(sandboxId);

// 3. Exec (streaming PTY — sync call, no await)
using var pty = MxcSdk.ExecInSandbox(sandboxId,
    new IsolationSessionExecConfig
    {
        Process = new ProcessConfig { CommandLine = "dir" }
    });
var exit = await pty.WaitForExitAsync();

// 3b. Exec (buffered — async)
var execResult = await MxcSdk.ExecInSandboxAsync(sandboxId,
    new IsolationSessionExecConfig
    {
        Process = new ProcessConfig { CommandLine = "echo done" }
    });

// 4. Stop
await MxcSdk.StopSandboxAsync(sandboxId);

// 5. Deprovision
await MxcSdk.DeprovisionSandboxAsync(sandboxId);

Platform support probing

Detect available containment backends on the current host:

PlatformSupport support = MxcSdk.GetPlatformSupport();
if (support.IsSupported)
{
    Console.WriteLine($"Backends: {string.Join(", ", support.AvailableMethods)}");
    Console.WriteLine($"Isolation tier: {support.IsolationTier}");
}

Error handling

The SDK throws MxcException when the native executor reports structured errors:

using Sabbour.Mxc.Sdk.Errors;

try
{
    var result = await MxcSdk.SpawnSandboxAsync("bad-cmd", policy);
}
catch (MxcException ex)
{
    Console.WriteLine($"Error code: {ex.Code}");
    Console.WriteLine($"Raw code: {ex.RawCode}");
    Console.WriteLine($"Message: {ex.Message}");
}

Error codes are defined in the ErrorCode enum (e.g., MalformedRequest, BackendUnavailable, StaleId).

Logging and diagnostics

The diagnostics layer exposes IMxcLogger and FileLogger for custom sinks. Spawn diagnostics are enabled through SandboxSpawnOptions.Debug; set LogDir when you want deterministic log placement. Sensitive tokens such as wamToken are redacted before they reach log files.

using Sabbour.Mxc.Sdk.Sandbox;

var options = new SandboxSpawnOptions
{
    Debug = true,
    LogDir = @"C:\mxc-logs"
};

Version support

This SDK validates the policy version field. The example pins 0.6.0-alpha, the schema shipped by the latest stable executor release (v0.6.1) — match it to the binary you install. The SDK accepts versions from 0.4.0-alpha (minimum) up to 0.7.0-alpha (the newest schema it understands); when you omit version, it fills in 0.7.0-alpha. Policies outside that range are rejected at config-creation time.

For the canonical field reference — version, filesystem, network, ui, and timeoutMs — see the upstream MXC Sandbox Policy Spec §5 (SandboxPolicy), pinned to the v0.6.1 release.

Running the tests

Tier 1: unit tests

dotnet test

Unit tests do not need the native executor and run on any OS supported by .NET 10.

Tier 2: integration/e2e tests

Download the prebuilt executor from microsoft/mxc releases. The latest release is v0.6.1, with the mxc-release-binaries.zip asset.

Unzip the archive, then set MXC_BIN_DIR to the folder that contains the architecture-specific executor directory (x64 or arm64):

$env:MXC_BIN_DIR = "C:\mxc-bin"
$env:MXC_INTEGRATION_TESTS = "1"
dotnet test

For test runs, prefer $env:MXC_BIN_DIR\x64\wxc-exec.exe or $env:MXC_BIN_DIR\arm64\wxc-exec.exe so the executor path is deterministic. On Windows, the default processcontainer backend needs Windows 11 24H2 or later (build 26100+) and does not require admin.

Troubleshooting

The SDK cannot find the executor

This package is a library, not a command-line tool. Sandbox execution shells out to the native MXC executor (wxc-exec.exe on Windows, lxc-exec on Linux, and mxc-exec-mac for macOS seatbelt).

Use one of these resolution paths:

  1. Set SandboxSpawnOptions.ExecutablePath for a single spawn.
  2. Set MXC_BIN_DIR to the root directory that contains <arch>\wxc-exec.exe on Windows, or <arch>/lxc-exec on Linux (<arch> is x64 or arm64).
  3. Package/publish layouts can include bin\<arch>\... next to the SDK assembly or app base directory.
  4. Development builds can be found under repo Cargo target paths.
  5. On Windows, PATH is a last fallback. Prefer ExecutablePath or MXC_BIN_DIR for predictable behavior.

Common errors

Symptom Cause Fix
wxc-exec.exe not found / lxc-exec not found The SDK cannot locate the executor. Set MXC_BIN_DIR or ExecutablePath (see above).
Bubblewrap (bwrap) is not installed or not on PATH The Linux process containment runs the workload under bwrap. sudo apt-get install -y bubblewrap.
E_NOTIMPL from the Windows base-container tier The Feature Store gate for the experimental kernel API is closed on this build. Enable the velocity keys with ViVeTool and reboot — see Enabling isolation backends.
iptables ... Permission denied (you must be root) Host-based outbound allowlisting (AllowedHosts) programs iptables, which needs CAP_NET_ADMIN. Run the host process elevated (sudo) on Linux/WSL.
Policy rejected at config creation The policy version is outside the supported range. Use a version between 0.4.0-alpha and 0.7.0-alpha — see Version support.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.2 2,388 6/10/2026
0.1.2-local 111 6/10/2026
0.1.1 115 6/10/2026