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
<PackageReference Include="Sabbour.Mxc.Sdk" Version="0.1.2" />
<PackageVersion Include="Sabbour.Mxc.Sdk" Version="0.1.2" />
<PackageReference Include="Sabbour.Mxc.Sdk" />
paket add Sabbour.Mxc.Sdk --version 0.1.2
#r "nuget: Sabbour.Mxc.Sdk, 0.1.2"
#:package Sabbour.Mxc.Sdk@0.1.2
#addin nuget:?package=Sabbour.Mxc.Sdk&version=0.1.2
#tool nuget:?package=Sabbour.Mxc.Sdk&version=0.1.2
Sabbour.Mxc.Sdk — unofficial, experimental .NET SDK for MXC (Microsoft eXecution Containers)
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-containeruses an experimental kernel API that ships behind a Windows Feature Store gate on current builds. When the gate is closed, the executor returnsE_NOTIMPLeven 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:61389575The 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-daclneeds a one-time host preparation that grants the AppContainer SIDs the ACEs they require. Runwxc-host-prepelevated (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:
Place the Linux executor where the SDK looks for it. The released
mxc-release-binaries.zipships the Windowswxc-exec.exe; on Linux the SDK resolveslxc-execfromMXC_BIN_DIR/<arch>/(<arch>isarm64orx64). Copy the Linuxlxc-execbuild 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"Install bubblewrap. The default
processcontainment runs the workload underbwrap. Without it the executor exits withBubblewrap (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.versiondefaults to the newest schema (0.7.0-alpha), which selects the velocity tier on Windows and fails if the keys above aren't enabled. Pin0.4.0-alphafor the AppContainer fallback. - Network host allow/block lists (
network.allowedHosts/network.blockedHosts) are not enforced on Windows — usenetwork.defaultPolicyornetwork.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
WSL2 installed and a Linux distribution set as default (Ubuntu 24.04 recommended).
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
workingDirectoryis 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 -dto prevent shell injection. - WslBubblewrap (recommended): confines the filesystem to the workspace directory (bind-mounted read-write), mounts
/usrand/etcread-only, creates/bin,/lib,/sbinsymlinks (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:
- Set
SandboxSpawnOptions.ExecutablePathfor a single spawn. - Set
MXC_BIN_DIRto the root directory that contains<arch>\wxc-exec.exeon Windows, or<arch>/lxc-execon Linux (<arch>isx64orarm64). - Package/publish layouts can include
bin\<arch>\...next to the SDK assembly or app base directory. - Development builds can be found under repo Cargo target paths.
- On Windows,
PATHis a last fallback. PreferExecutablePathorMXC_BIN_DIRfor 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 | Versions 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. |
-
net10.0
- NuGet.Versioning (>= 6.13.2)
- Porta.Pty (>= 1.0.7)
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 |