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 aphase: "provision"envelope and returns aProvisionResultcontaining a brandedSandboxIdand backend-specific metadatastartSandbox– Sends aphase: "start"envelope identified by the sandbox IDexecInSandbox/execInSandboxAsync– Sends aphase: "exec"envelope with aProcessConfig; the async variant buffers stdout/stderr and parses error envelopesstopSandbox– Sends aphase: "stop"envelope; the sandbox remains provisioned for later restartsdeprovisionSandbox– Sends aphase: "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:
- Lifecycle methods –
sdk/src/state-aware.ts - Type definitions –
sdk/src/state-aware-types.ts - Envelope construction –
sdk/src/state-aware-helper.ts
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
ChildProcessobjects suitable for real-time I/O. State-Aware returns structured objects likeProvisionResult,StartResult, andExecResultcontaining sandbox identifiers and metadata. -
Error Semantics: One-Shot relies on exit codes. State-Aware provides deserialized
MxcErrorobjects with full error context throughexecInSandboxAsync. -
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.tsprovides ephemeral execution viaspawnSandboxandspawnSandboxFromConfigwith automatic cleanup, ideal for short-lived workloads. - The State-Aware API in
sdk/src/state-aware.tsoffers granular lifecycle control throughprovisionSandbox,startSandbox,execInSandbox*,stopSandbox, anddeprovisionSandbox. - Both patterns ultimately invoke the native
mxcbinary throughresolveBinaryAndCommonArgsas implemented inhelper.ts. - State-Aware sandboxes use strongly-typed
SandboxId<C>branding defined instate-aware-types.tsto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →