MXC TypeScript SDK: One-Shot vs State-Aware API Patterns

The MXC TypeScript SDK provides two distinct programming models for sandbox execution: the One-Shot API for ephemeral, run-and-forget workloads via spawnSandbox, and the State-Aware API for long-running, reusable sandboxes with explicit lifecycle management through methods like provisionSandbox and execInSandbox.

The microsoft/mxc repository ships a TypeScript SDK that abstracts container-style sandbox execution behind two fundamentally different architectural patterns. While both APIs ultimately invoke the same native mxc binary through resolveBinaryAndCommonArgs in helper.ts, they differ significantly in resource management, error handling, and type safety guarantees.

Understanding the One-Shot API Pattern

The One-Shot API, implemented in sdk/src/sandbox.ts, is designed for scenarios where sandbox provisioning time is acceptable for each individual invocation and lifecycle management is unnecessary.

Core Implementation and Entry Points

The primary entry points are spawnSandbox and spawnSandboxFromConfig. These functions construct a single envelope describing the entire request—including the command line, filesystem policies, network configuration, and UI settings—and pass it directly to the native binary. The binary creates the sandbox, streams output via a ChildProcess or PTY interface, and automatically tears down the environment when the process exits.

Error handling in this pattern is process-level only. Failures surface as non-zero exit codes on the returned process object, without structured error deserialization or detailed exception metadata.

When to Use the One-Shot Pattern

Choose this pattern for:

  • Quick "run-and-forget" scripts where the overhead of sandbox creation per call is acceptable
  • Simple CI steps where spin-up time dominates and persistence is unnecessary
  • Scenarios where you do not need to keep a sandbox alive between distinct command executions

One-Shot Implementation Example

import { spawnSandbox, SandboxSpawnOptions } from "mxc/sdk";

// Run `echo hello` inside a temporary sandbox
const options: SandboxSpawnOptions = {
  // optional: limit resources, set env vars, etc.
};
const pty = spawnSandbox(
  {
    // Minimal sandbox config – filesystem/network/ UI omitted for brevity
    commandLine: "echo hello",
  },
  options,
);

// Pipe live output to the console
pty.onData((data) => process.stdout.write(data));
pty.onExit(({ exitCode }) => console.log(`Exited with ${exitCode}`));

Source: spawnSandbox implementation lives in sdk/src/sandbox.ts.

Understanding the State-Aware API Pattern

The State-Aware API, located in sdk/src/state-aware.ts, exposes a granular, five-phase lifecycle for long-running sandboxes that require stable identity and repeated execution.

Lifecycle Management Methods

Unlike the monolithic One-Shot approach, the State-Aware API breaks operations into discrete async helpers:

  • provisionSandbox – Sends a phase: "provision" envelope and returns a ProvisionResult containing a branded SandboxId and backend-specific metadata
  • startSandbox – Sends a phase: "start" envelope identified by the sandbox ID
  • execInSandbox / execInSandboxAsync – Sends a phase: "exec" envelope with a ProcessConfig; the async variant buffers stdout/stderr and parses error envelopes
  • stopSandbox – Sends a phase: "stop" envelope; the sandbox remains provisioned for later restarts
  • deprovisionSandbox – Sends a phase: "deprovision" envelope; invalidates the sandbox ID permanently

All envelopes are constructed by buildStateAwareEnvelope in sdk/src/state-aware-helper.ts and dispatched via nonExecCall (for non-exec phases) or spawnAndCollect / PTY spawning (for exec phases).

Type Safety and Backend Branding

A critical advantage of the State-Aware API is its compile-time type safety. The SDK defines a branded type SandboxId<C> in sdk/src/state-aware-types.ts, where the generic C represents the specific backend (e.g., "isolation_session"). This branding ensures that configuration objects using conditional types like ProvisionConfigFor<C> or StartConfigFor<C> cannot be mismatched across different backends, preventing configuration errors at compile time rather than runtime.

Structured Error Handling with MxcError

Unlike the One-Shot pattern's process-level failures, the State-Aware API deserializes errors into strongly-typed MxcError objects. The execInSandboxAsync method specifically captures error envelopes containing the original error code, message, and details, throwing typed exceptions that enable robust programmatic error handling for stateful services.

State-Aware Implementation Example

import {
  provisionSandbox,
  startSandbox,
  execInSandboxAsync,
  stopSandbox,
  deprovisionSandbox,
} from "mxc/sdk";
import type { SandboxId } from "mxc/sdk/src/state-aware-types";

// 1️⃣ Provision a sandbox (here we use the isolation‑session backend)
const prov = await provisionSandbox("isolation_session", {
  filesystem: { readonly: true },
});
const sandboxId: SandboxId<"isolation_session"> = prov.sandboxId;

// 2️⃣ Start the sandbox
await startSandbox(sandboxId);

// 3️⃣ Execute a command (buffered version)
const execResult = await execInSandboxAsync(sandboxId, {
  process: { commandLine: "echo state‑aware" },
});
console.log(execResult.stdout.trim()); // → “state-aware”

// 4️⃣ Stop (optional – allows later restart)
await stopSandbox(sandboxId);

// 5️⃣ De‑provision when finished
await deprovisionSandbox(sandboxId);

Key source files:

Architectural Comparison

Both APIs utilize the underlying mxc native binary, but differ in execution semantics:

  • Resource Model: One-Shot creates and destroys a sandbox for every call, incurring provisioning overhead per execution. State-Aware provisions once, allowing multiple start/exec/stop cycles to amortize provisioning costs across many commands.

  • Return Types: One-Shot returns streaming ChildProcess objects suitable for real-time I/O. State-Aware returns structured objects like ProvisionResult, StartResult, and ExecResult containing sandbox identifiers and metadata.

  • Error Semantics: One-Shot relies on exit codes. State-Aware provides deserialized MxcError objects with full error context through execInSandboxAsync.

  • Type Safety: Only the State-Aware API uses branded SandboxId<C> types and backend-specific configuration interfaces to prevent cross-backend contamination at compile time.

Summary

  • The One-Shot API in sdk/src/sandbox.ts provides ephemeral execution via spawnSandbox and spawnSandboxFromConfig with automatic cleanup, ideal for short-lived workloads.
  • The State-Aware API in sdk/src/state-aware.ts offers granular lifecycle control through provisionSandbox, startSandbox, execInSandbox*, stopSandbox, and deprovisionSandbox.
  • Both patterns ultimately invoke the native mxc binary through resolveBinaryAndCommonArgs as implemented in helper.ts.
  • State-Aware sandboxes use strongly-typed SandboxId<C> branding defined in state-aware-types.ts to enforce backend-specific configuration safety.
  • Choose One-Shot for CI steps and isolated commands; choose State-Aware for persistent services, repeated executions, and scenarios requiring stable sandbox identity such as Entra-backed isolation sessions.

Frequently Asked Questions

When should I use the One-Shot API versus the State-Aware API?

Use the One-Shot API when you need to execute a single command without managing sandbox lifetime, such as in CI pipelines or simple automation scripts where provisioning overhead is acceptable per invocation. Choose the State-Aware API when you need to reuse a sandbox across multiple commands, maintain state between executions, or when working with backends like Entra-backed isolation sessions that require a stable sandbox identity across distinct lifecycle phases.

How does error handling differ between the two patterns?

The One-Shot API surfaces failures only as process-level exit codes on the returned ChildProcess or PTY object. In contrast, the State-Aware API's execInSandboxAsync method deserializes error envelopes into structured MxcError objects containing specific error codes, messages, and details, enabling programmatic error handling rather than parsing shell output or checking exit codes.

What is the purpose of the branded SandboxId type?

The SandboxId<C> type in sdk/src/state-aware-types.ts uses TypeScript's type system to "brand" a sandbox ID with its specific backend configuration type (e.g., "isolation_session"). This compile-time guarantee ensures that you cannot accidentally pass configuration options meant for one backend to a sandbox provisioned for another, preventing subtle runtime configuration mismatches.

Can I switch from One-Shot to State-Aware execution for an existing sandbox?

No, these patterns are mutually exclusive. One-Shot sandboxes created via spawnSandbox are transient by design and are immediately deprovisioned upon process exit. To utilize State-Aware lifecycle management, you must explicitly provision a new sandbox using provisionSandbox and manage its entire lifecycle—from provisioning through deprovisioning—using the discrete phase methods in sdk/src/state-aware.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →