@microsoft/mxc-sdk

August 22, 2026 · View on GitHub

Node.js / TypeScript SDK for MXC (Microsoft eXecution Containers) — a policy-driven sandbox for running untrusted code (model output, plugins, tools) on Windows, Linux, and macOS.

Status: Public Preview. Schemas and APIs may change between minor versions until 1.0.

npm install @microsoft/mxc-sdk
import {
  spawnSandboxFromConfig, createConfigFromPolicy,
  getAvailableToolsPolicy, getTemporaryFilesPolicy,
  getPlatformSupport,
} from '@microsoft/mxc-sdk';

if (!getPlatformSupport().isSupported) {
  throw new Error('MXC not available on this host');
}

// Discover host tools (python, node, etc.) and a writable temp dir.
const tools = getAvailableToolsPolicy(process.env);
const temp  = getTemporaryFilesPolicy();

const config = createConfigFromPolicy({
  version: '0.6.0-alpha',
  filesystem: {
    readonlyPaths:  tools.readonlyPaths,    // PATH, PYTHONPATH, JAVA_HOME, …
    readwritePaths: temp.readwritePaths,    // %TEMP% / $TMPDIR
  },
  network: { allowOutbound: false },
  timeoutMs: 30_000,
});
config.process!.commandLine = 'python -c "print(\'hello from sandbox\')"';

const child = spawnSandboxFromConfig(config, { usePty: false });
child.stdout!.on('data', (d) => process.stdout.write(d));
child.on('close', (code) => console.log('exit:', code));

Compatibility

Policy / config schema versions:

VersionStatusSchema file
0.4.0-alphaRetired — below the 0.6.0-alpha floor (no longer accepted)schemas/stable/mxc-config.schema.0.4.0-alpha.json
0.5.0-alphaRetired — below the 0.6.0-alpha floor (no longer accepted)schemas/stable/mxc-config.schema.0.5.0-alpha.json
0.6.0-alphaStable (minimum supported)schemas/stable/mxc-config.schema.0.6.0-alpha.json
0.7.0-alphaStableschemas/stable/mxc-config.schema.0.7.0-alpha.json
0.8.0-alphaStable (current)schemas/stable/mxc-config.schema.0.8.0-alpha.json
0.9.0-alphaDev (experimental backends, the experimental.* block, state-aware sandbox lifecycle)schemas/dev/mxc-config.schema.0.9.0-dev.json

Pick 0.8.0-alpha for new code on any supported platform.

Stable schemas document only the non-experimental surface. Experimental backends (windows_sandbox, wslc, microvm, hyperlight, isolation_session), the experimental.* block, and state-aware lifecycle live in 0.9.0-dev. The parser still accepts them when paired with --experimental regardless of which schema your config validates against — schema choice affects editor validation, not runtime behavior.

Network host allow/block lists are not implemented on Windows. network.allowedHosts / network.blockedHosts have no enforcement on this platform — use network.defaultPolicy (allow / block) or network.proxy to constrain network access.

Schema 0.8 directional networking: createConfigFromPolicy accepts network.egress / network.ingress, runtimeConfig.networkProxy, and processContainer.network.allowedProxyPeer. Do not mix those fields with the legacy network.allowOutbound, network.allowLocalNetwork, network.allowedHosts, network.blockedHosts, or network.proxy fields. createConfigFromPolicy authors either shape according to the supplied policy version and fields. Schema 0.6 and 0.7 policies continue to produce the legacy wire shape. With schema 0.8, omitting all network fields leaves the network block out of the generated config; the native parser interprets that as directional default-deny for egress, ingress, and host loopback. See the Sandbox Policy 0.8.0 specification for the complete cross-platform authoring shape.

Model 1 permits direct connections selected by IP/CIDR, protocol, and port rules; it does not configure an application-layer proxy. Model 2 denies direct internet access and supplies a loopback HTTP/S proxy endpoint. Backend-specific requirements determine how that proxy endpoint is made reachable.

Simple model 1 example — direct egress with L3/L4 filtering:

import {
  createConfigFromPolicy,
  spawnSandboxFromConfig,
} from '@microsoft/mxc-sdk';

const directConfig = createConfigFromPolicy({
  version: '0.8.0-alpha',
  network: {
    egress: {
      default: 'deny',
      allow: [{
        to: [{ cidr: '192.0.2.0/24' }],
        ports: [{ protocol: 'tcp', port: 443 }],
      }],
    },
    ingress: { default: 'deny', hostLoopback: 'deny' },
  },
});
directConfig.process!.commandLine = 'node agent.js';
spawnSandboxFromConfig(directConfig);

Simple model 2 example — loopback HTTP/S proxy:

const proxyConfig = createConfigFromPolicy({
  version: '0.8.0-alpha',
  network: {
    egress: { default: 'deny' },
    ingress: { default: 'deny', hostLoopback: 'deny' },
  },
  runtimeConfig: { networkProxy: 'http://127.0.0.1:8080' },
});
proxyConfig.process!.commandLine = 'node agent.js';
spawnSandboxFromConfig(proxyConfig);

These are example configurations rather than universal backend recipes. ProcessContainer proxy configurations have additional criteria; see the ProcessContainer 0.8 proxy example. See the networking specification for all three connectivity modes and backend-specific support.

Platforms:

PlatformDefault backendOther backendsMinimum build
Windows 11 24H2+ (verified on 25H2)processcontainerwindows_sandbox, wslc, microvm, isolation_sessionprocesscontainer: 26100 (24H2)
isolation_session: 26340.9212 (Insider Preview)
Linux x64 / ARM64bubblewraplxc
macOS ARM64 (schema 0.7.0-alpha+)seatbelt

The default processcontainer, bubblewrap, lxc, and seatbelt backends work out of the box. Experimental backends (windows_sandbox, wslc, microvm, isolation_session, hyperlight) require { experimental: true } in SandboxSpawnOptions when you spawn — see Choosing a Backend.

Hyperlight is an opt-in build flavor (Linux x64 and Windows x64) gated by the --with-hyperlight cargo feature. Default shipped binaries do not include it; build from source with build.bat --with-hyperlight (Windows) or the equivalent cargo invocation on Linux.

getPlatformSupport() reports backend availability and, when the native probe can determine it, uiCapabilities: a platform-neutral view of which UI restrictions the host can enforce. This is currently populated only by the Windows native probe, where it is derived from JOB_OBJECT_UILIMIT_* support; Linux and macOS omit the field until their probes expose equivalent data.

Node.js: ≥ 18.


Three Ways to Spawn

The SDK provides three entry points. Prefer the config-based path (createConfigFromPolicy + spawnSandboxFromConfig) — it gives you backend selection, backend-specific tuning, and (with usePty: false) separated stdout/stderr.

import {
  createConfigFromPolicy, spawnSandboxFromConfig,
  getAvailableToolsPolicy, getTemporaryFilesPolicy,
} from '@microsoft/mxc-sdk';

const tools = getAvailableToolsPolicy(process.env);
const temp  = getTemporaryFilesPolicy();

const config = createConfigFromPolicy(
  {
    version: '0.6.0-alpha',
    filesystem: {
      readonlyPaths:  tools.readonlyPaths,
      readwritePaths: temp.readwritePaths,
    },
    network: { allowOutbound: true },
    timeoutMs: 30_000,
  },
  'process', // intent: "process" | "vm" | "microvm"
);

// Add the script and any backend-specific runtime settings on the returned config.
config.process!.commandLine = 'python script.py';

// PTY mode (default) — IPty, merged stdout+stderr
const pty = spawnSandboxFromConfig(config);
pty.onData((d) => process.stdout.write(d));
pty.onExit(({ exitCode }) => console.log('exit:', exitCode));

// Pipe mode — ChildProcess with separated stdout/stderr + reliable exit codes
const child = spawnSandboxFromConfig(config, { usePty: false });
child.stdout!.on('data', (d) => process.stdout.write(d));
child.stderr!.on('data', (d) => process.stderr.write(d));
child.on('close', (code) => console.log('exit:', code));

2. spawnSandbox(script, policy, ...) — convenience

Quick path for process-isolation only (processcontainer on Windows, lxc on Linux, seatbelt on macOS). Returns a node-pty IPty with merged stdout/stderr.

import {
  spawnSandbox,
  getAvailableToolsPolicy, getTemporaryFilesPolicy,
} from '@microsoft/mxc-sdk';

const tools = getAvailableToolsPolicy(process.env);
const temp  = getTemporaryFilesPolicy();

const pty = spawnSandbox('python script.py', {
  version: '0.6.0-alpha',
  filesystem: {
    readonlyPaths:  tools.readonlyPaths,
    readwritePaths: temp.readwritePaths,
  },
  timeoutMs: 30_000,
});
pty.onData((d) => process.stdout.write(d));
pty.onExit(({ exitCode }) => console.log('exit:', exitCode));

3. spawnSandboxAsync(script, policy, ...) — promise-style

The await-friendly version of spawnSandbox. Same arguments, same restriction (process-isolation only), but resolves with { stdout, stderr, exitCode } instead of returning an IPty. stderr is always '' because the underlying PTY merges streams.

import {
  spawnSandboxAsync,
  getAvailableToolsPolicy, getTemporaryFilesPolicy,
} from '@microsoft/mxc-sdk';

const tools = getAvailableToolsPolicy(process.env);
const temp  = getTemporaryFilesPolicy();

const result = await spawnSandboxAsync(
  'python -c "import sys; print(sys.version)"',
  {
    version: '0.6.0-alpha',
    filesystem: {
      readonlyPaths:  tools.readonlyPaths,
      readwritePaths: temp.readwritePaths,
    },
    timeoutMs: 30_000,
  },
);
console.log(result.stdout);

Tip: for agentic workloads, prefer multiple narrow sandboxes (one policy per task step) over a single broad policy. Add task-specific paths on top of the discovered base (e.g. a scoped output directory in readwritePaths, a project source tree in readonlyPaths, secrets in deniedPaths).


Choosing a Backend

Table of all backends and links to per-backend guides — click to expand.

SandboxPolicy is cross-platform. The backend is selected by the second argument to createConfigFromPolicy(policy, containment). Pass an abstract intent ("process", "vm", "microvm") whenever possible — the SDK and native binary resolve it to the right concrete backend for the host. Pass a concrete backend name when you need a specific runner.

BackendIntentPlatformsStable?Guide
processcontainerprocessWindowsdocs/process-container/guide.md
bubblewrapprocessLinuxdocs/bwrap-support/bubblewrap-backend.md
lxc(concrete only)Linuxdocs/lxc-support/lxc-backend.md
seatbeltprocessmacOS✅ (schema 0.7.0-alpha+)docs/seatbelt/seatbelt-backend.md
windows_sandboxvmWindowsExperimentaldocs/windows-sandbox/windows-sandbox.md
microvmmicrovmWindowsExperimentaldocs/nanvix-microvm/nanvix.md — MicroVM via NanVix on Windows Hypervisor Platform
wslc(concrete only)WindowsExperimentaldocs/wsl/wsl-container-getting-started.md
isolation_session(concrete only)WindowsExperimentaldocs/isolation-session/oneshot.md

Experimental backends require { experimental: true } in SandboxSpawnOptions:

const config = createConfigFromPolicy(policy, 'vm'); // → windows_sandbox on Windows
config.process!.commandLine = 'cmd /c whoami';
const pty = spawnSandboxFromConfig(config, { experimental: true });

Backend-specific tuning lives on the returned ContainerConfig. The full set of fields per backend is in the JSON schemas — they're the source of truth:

Open the schema file matching your policy.version (e.g. mxc-config.schema.0.6.0-alpha.json) and look up processContainer, lxc, experimental.wslc, experimental.windows_sandbox, etc.

For Windows ProcessContainer configs, processContainer.learningMode: true enables deny-and-record learning mode: failed accesses are logged but remain denied. The internal learningModeLogging and permissiveLearningMode capability names are reserved and must not be added directly to processContainer.capabilities.

State-Aware Sandboxes

Provision once, exec many, tear down (long-lived workflows) — click to expand.

For long-lived sandboxes where you provision once, exec many times, and tear down at the end (e.g. agentic loops), use the state-aware lifecycle.

Backend support: the state-aware lifecycle is currently implemented for isolation_session, windows_sandbox, and wslc (all Windows-only; all still experimental, so every call must pass { experimental: true }). The one-shot spawn APIs (spawnSandbox / spawnSandboxFromConfig) are the supported path for every other backend.

import {
  provisionSandbox, startSandbox, execInSandboxAsync,
  stopSandbox, deprovisionSandbox,
} from '@microsoft/mxc-sdk';

// Every call takes a single options object (3rd arg). Experimental backends
// must pass `experimental: true`.
// isolation_session provision requires the unrestricted-network acknowledgment:
// the container's network cannot be filtered or denied, so you must opt in.
const { sandboxId } = await provisionSandbox(
  'isolation_session',
  { network: { defaultPolicy: 'allow', allowLocalNetwork: true } },
  { experimental: true },
);
const opts = { experimental: true };

await startSandbox(sandboxId, undefined, opts);

const r1 = await execInSandboxAsync(sandboxId, { process: { commandLine: 'echo hello' } }, opts);
const r2 = await execInSandboxAsync(sandboxId, { process: { commandLine: 'whoami' } }, opts);

await stopSandbox(sandboxId, undefined, opts);
await deprovisionSandbox(sandboxId, undefined, opts);

windows_sandbox follows the same shape (substitute the containment string and provide filesystem.readwritePaths / readonlyPaths at provision if needed). See docs/windows-sandbox/windows-sandbox.md for the per-phase config matrix.

wslc follows the same shape and needs no provision config at all (it defaults to an alpine:latest container with no network). Provide filesystem.readwritePaths / readonlyPaths (mounted for the sandbox's lifetime), network.defaultPolicy: 'allow' (a bridged container; the default 'block' gives no network), and/or a backend-specific image / imageTarPath at provision; inject a cooperative network.proxy: { url } per-exec. WSLc state-aware requests default to schema 0.8.0-alpha. See docs/wsl/wslc-state-aware.md for the per-phase config matrix.

Handling failures. Every lifecycle call rejects with a typed MxcError. Branch on code first; when the failure came from an underlying platform API, the error also carries discrete diagnostic fields rather than a prose blob:

import { MxcError } from '@microsoft/mxc-sdk';

try {
  await startSandbox(sandboxId, {}, { experimental: true });
} catch (err) {
  if (err instanceof MxcError) {
    if (err.code === 'stale_id') { /* the sandbox is gone -- re-provision */ }
    console.error(err.message);      // bare, human-readable
    console.error(err.operation);    // e.g. 'IsoSessionOps.StartSessionAsync'
    console.error(err.nativeCode);   // e.g. '0x80070490'
    console.error(err.remediation);  // the API's own fix-it hint, when it supplies one
  }
}

operation, nativeCode and remediation are optional. A failure MXC raises before reaching the backend — a malformed request or id, or a policy rejection — carries only code and message.

These three are currently populated only by IsolationSession state-aware operations. Windows Sandbox has no semantic error channel to derive them from, and the one-shot surface folds the same detail into message instead, so they are uniformly absent there — always treat them as optional.

Branch program logic on code, which is a closed, versioned union. The values of operation and nativeCode are best-effort diagnostics derived from the underlying platform API and may change without a version bump — use them for telemetry, logging and diagnosis rather than control flow.

Full design and API: docs/state-aware-lifecycle/.

Policy Discovery Helpers

Auto-enumerate host tools, profile, and temp dirs — click to expand.

The SDK ships helpers that enumerate the host environment so your policy stays portable:

import {
  getAvailableToolsPolicy, getUserProfilePolicy, getTemporaryFilesPolicy,
} from '@microsoft/mxc-sdk';

const tools   = getAvailableToolsPolicy(process.env); // PATH, PYTHONPATH, JAVA_HOME, …
const profile = getUserProfilePolicy();               // %LOCALAPPDATA%\Programs, ~/.local/*
const tmp     = getTemporaryFilesPolicy();            // %TEMP% / $TMPDIR

const policy = {
  version: '0.6.0-alpha',
  filesystem: {
    readonlyPaths: [...tools.readonlyPaths, ...profile.readonlyPaths],
    readwritePaths: tmp.readwritePaths,
  },
  network: { allowOutbound: false },
};

Each helper returns { readonlyPaths, readwritePaths } — merge what you want into SandboxPolicy.filesystem.


Common Pitfalls

UI is blocked by default on 0.5.0+ — some shells need it

The policy.ui block is enforced on all supported schema versions, and policy.ui.allowWindows defaults to false. Most non-interactive command-line tools work fine, but on Windows some shells make win32k system calls during startup and fail without UI access. All versions of PowerShell are affected — both Windows PowerShell 5.1 (powershell.exe) and PowerShell 7 (pwsh.exe). Set ui.allowWindows: true when launching a shell:

import { spawnSandboxFromConfig, createConfigFromPolicy } from '@microsoft/mxc-sdk';

const config = createConfigFromPolicy({
  version: '0.6.0-alpha',
  ui: { allowWindows: true },     // ← required for powershell.exe to start
});
config.process!.commandLine = 'powershell.exe -NoProfile -Command "Get-Date"';

const child = spawnSandboxFromConfig(config, { usePty: false });

PTY APIs merge stdout and stderr

spawnSandbox and spawnSandboxAsync use a PTY, so stderr is always empty in their result. Use spawnSandboxFromConfig(config, { usePty: false }) for separated streams.

createConfigFromPolicy leaves commandLine empty

You must set config.process!.commandLine = '…' before calling spawnSandboxFromConfig.

Default-deny applies to everything

No network field → no network. No readwritePaths → process can't write %TEMP%. No ui → no GUI. Use the discovery helpers to compose a sensible baseline.

process.cwd doesn't grant filesystem access

Setting cwd (or the workingDirectory argument) does not add that path to the policy. Add it to readonlyPaths / readwritePaths explicitly.


Troubleshooting

Common errors and what they mean — click to expand.
ErrorCauseFix
MXC is not supported on this platformgetPlatformSupport() returned isSupported: false. On Linux: neither LXC nor Bubblewrap on PATH. On macOS: schema version < 0.6.0-alpha.Install LXC/Bubblewrap, or switch to schema 0.6.0-alpha (or 0.7.0-alpha if you need state-aware lifecycle).
wxc-exec.exe not found / lxc-exec not foundThe SDK couldn't locate the native binary.Set MXC_BIN_DIR=<dir> so <dir>/<arch>/wxc-exec.exe (or lxc-exec) exists, or pass options.executablePath explicitly.
Invalid containment value '<x>'containment field doesn't match the parser's accepted values.Use one of the abstract intents (process, vm, microvm) or a concrete backend listed in Choosing a Backend.
'<x>' containment requires experimental modeA windows_sandbox / wslc / microvm / isolation_session / hyperlight backend was selected without the flag.Pass { experimental: true } in SandboxSpawnOptions.
process.commandLine starts with an unquoted Windows path containing a spacewxc-exec rejects unquoted paths with spaces at parse time.Quote the executable: '"C:\\Program Files\\…\\foo.exe" args'.
Experimental_CreateProcessInSandbox failed: WIN32_ERROR(...)Native sandbox API returned an OS-level error, e.g. 448 = device feature not supported (Windows build / WIP feature not enabled). Note 120 (call not implemented / BaseContainer disabled) is now handled automatically — the default process backend falls back to AppContainer+DACL, so it no longer surfaces here.Check the Windows build / WIP requirements for the backend you selected.
Process exits -1 / 4294967295 with no stdoutNative binary terminated abnormally.Re-run with options.debug: true (or options.logDir: '<dir>') to capture diagnostic logs.
policy.version '<x>' is older than supported / newer than supportedVersion is outside the SDK's accepted range.Use 0.6.0-alpha, 0.7.0-alpha, 0.8.0-alpha, or 0.9.0-alpha. See Compatibility.

For backend-specific errors, see the per-backend guide linked from the Choosing a Backend table.


API Surface

Every export at a glance — click to expand.
// Spawn — config-based (recommended)
createConfigFromPolicy(policy, containment?, containerName?) → ContainerConfig
spawnSandboxFromConfig(config, options?, workingDirectory?, env?) → IPty | ChildProcess

// Spawn — convenience (process containment only)
spawnSandbox(script, policy, options?, workingDirectory?, containerName?, env?) → IPty
spawnSandboxAsync(script, policy, ...) → Promise<{ stdout, stderr, exitCode }>

// State-aware lifecycle (currently `isolation_session`, `windows_sandbox`, and `wslc` — all Windows-only)
// `config` on provisionSandbox is required for backends whose provision config
// has a required member (isolation_session: the network acknowledgment) and
// optional otherwise (windows_sandbox, wslc).
provisionSandbox(containment, config, options?)  → Promise<ProvisionResult>
startSandbox(sandboxId, config?, options?)       → Promise<StartResult>
execInSandbox(sandboxId, config, options?)       → IPty             // streaming
execInSandboxAsync(sandboxId, config, options?)  → Promise<ExecResult>
stopSandbox(sandboxId, config?, options?)        → Promise<StopResult>
deprovisionSandbox(sandboxId, config?, options?) → Promise<DeprovisionResult>

// Platform & policy discovery
getPlatformSupport() → PlatformSupport
getAvailableToolsPolicy(env?, options?) → FilesystemPolicyResult
getUserProfilePolicy()                  → FilesystemPolicyResult
getTemporaryFilesPolicy(env?)           → FilesystemPolicyResult

// Capability types
UiCapabilitySupport

// Errors (typed wire-format errors from wxc-exec)
ErrorCode, MxcError, MxcErrorFields
mxcErrorFromCode(code, message, details?)   → MxcError

Full TypeScript definitions ship with the package (dist/index.d.ts). All exports are named exports from @microsoft/mxc-sdk.


Telemetry is off-by-default unless the caller opts in with top-level telemetry.enabled: true and the applicable Windows consent/policy gates permit collection.

Telemetry consent behavior follows docs/telemetry/telemetry-consent-design.md: the SDK stays UI-agnostic, renders the canonical resource verbatim through a host presenter, persists only explicit yes/no decisions, treats dismissal and failures as non-grants, and never lets policy or transport failures opt a user in.

Administrative policy

An IT administrator can still block MXC telemetry device-wide via MXC's own registry policy setting. See docs/telemetry/telemetry-administrative-policy.md for the stable registry contract and interaction rules.


Further Reading


License

MIT. Contributions welcome — see the main MXC repository.