How MXC's State-Aware Sandbox Lifecycle Works: The 5-Phase Architecture Explained

MXC's state-aware sandbox lifecycle replaces the monolithic one-shot spawnSandbox* model with five discrete phases—provision, start, exec, stop, and deprovision—enabling granular, independent control through a stateless Rust executor and a typed TypeScript SDK.

The Microsoft Compute eXtension (microsoft/mxc) introduces a state-aware API that runs alongside its classic sandbox spawning routines. Instead of provisioning, starting, executing, stopping, and deprovisioning a sandbox in a single call, developers can now invoke each phase independently via thin SDK wrappers and a backend trait system. This architecture keeps MXC itself stateless while delegating persistence to individual backends such as the isolation_session provider.

The Five Phases of the State-Aware Lifecycle

The state-aware surface exposes five distinct state transitions. Each phase accepts a specific configuration and operates on a shared opaque handle called sandboxId.

  1. Provision — Transitions a sandbox from not-provisioned to provisioned and returns a sandboxId plus optional metadata.
  2. Start — Transitions from provisioned to running using backend-specific startup configuration.
  3. Exec — Runs while the sandbox is running and returns stdout, stderr, and exitCode.
  4. Stop — Transitions from running back to provisioned, preserving any persisted state.
  5. Deprovision — Transitions from provisioned to not-provisioned, releasing all resources and invalidating the handle.

These phases are formally defined in the design document at docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md §4 — Lifecycle model. The specification mandates that callers must sequence requests according to valid state transitions; otherwise, the backend returns a typed error such as not_started.

Opaque SandboxId and Backend Routing

When provisionSandbox is invoked, the backend returns an opaque string branded as SandboxId<C> in the SDK. A typical identifier uses the format iso:abcd1234, where the prefix (iso) encodes the backend type and the remainder is opaque to MXC.

This prefix design allows the Rust dispatcher to infer the target backend from the identifier alone without consulting external state. For example, the isolation_session backend declares ID_PREFIX = "iso" and BACKEND_KEY = "isolation_session" in src/backends/isolation_session/common/src/state_aware.rs. The dispatch logic that performs this routing lives in src/core/wxc_common/src/state_aware_dispatch.rs.

TypeScript SDK Surface

The @microsoft/mxc-sdk package exposes one async function per lifecycle phase, with all signatures declared in sdk/src/types.ts and operating on the branded sandboxId.

// Provision – chooses a backend once
const { sandboxId } = await provisionSandbox('isolation_session', provisionConfig, opts);

// Subsequent calls infer the backend from the branded ID
await startSandbox(sandboxId, startConfig, opts);
const execResult = await execInSandboxAsync(sandboxId, execConfig, opts);
await stopSandbox(sandboxId, {}, opts);
await deprovisionSandbox(sandboxId, {}, opts);

Because the SDK brands the returned handle as SandboxId<C>, later calls automatically narrow to the correct backend type without requiring the caller to resupply the backend name. This branding eliminates the need to pass the backend key on every subsequent call.

Wire Contract and JSON Envelope

Each SDK call serializes to JSON and is delivered to the executor (wxc-exec or lxc-exec) through the existing --config-base64 CLI flag. The JSON envelope carries a top-level phase discriminator and either a containment field for provision or a sandboxId field for all other phases.

Cross-cutting configuration such as filesystem, network, and ui is passed at the top level of the envelope. Backend-specific parameters are nested under experimental.<backend>.<phase>. This contract is documented in docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md and parsed by the Rust executor into a RawStateAwareRequest defined in src/core/wxc_common/src/config_parser.rs.

Rust Executor and the StatefulSandboxBackend Trait

The Rust executor parses the incoming JSON into RawStateAwareRequest and dispatches to a backend that implements the StatefulSandboxBackend trait. The trait definition resides in src/core/wxc_common/src/state_aware_backend.rs.

pub trait StatefulSandboxBackend {
    const ID_PREFIX: &'static str;
    const BACKEND_KEY: &'static str;
    type ProvisionConfig: serde::de::DeserializeOwned;
    type StartConfig: serde::de::DeserializeOwned;
    type ExecConfig: serde::de::DeserializeOwned;
    type StopConfig: serde::de::DeserializeOwned;
    type DeprovisionConfig: serde::de::DeserializeOwned;

    fn provision(&mut self, request: &ExecutionRequest,
                 config: Option<Self::ProvisionConfig>)
        -> Result<ProvisionResult<Self::ProvisionMetadata>, MxcError>;

    fn start(&mut self, sandbox_id: &str, request: &ExecutionRequest,
             config: Option<Self::StartConfig>)
        -> Result<StartResult<Self::StartMetadata>, MxcError>;

    fn exec(&mut self, sandbox_id: &str, request: &ExecutionRequest,
            config: Option<Self::ExecConfig>)
        -> Result<ExecHandle, MxcError>;

    fn stop(&mut self, sandbox_id: &str, request: &ExecutionRequest,
            config: Option<Self::StopConfig>)
        -> Result<StopResult<Self::StopMetadata>, MxcError>;

    fn deprovision(&mut self, sandbox_id: &str, request: &ExecutionRequest,
                   config: Option<Self::DeprovisionConfig>)
        -> Result<DeprovisionResult<Self::DeprovisionMetadata>, MxcError>;
}

The trait supplies a default provision implementation that mints IDs of the form <ID_PREFIX>:<random> when a backend has no native allocation work. Backends may override this behavior, and they may leave start, stop, and deprovision as no-ops; only exec is mandatory. The default provision logic is located at lines 222–236 of src/core/wxc_common/src/state_aware_backend.rs.

Reference Backend: Isolation Session

The first state-aware-capable backend is the isolation_session runner, implemented in src/backends/isolation_session/common/src/state_aware.rs. It sets ID_PREFIX = "iso" and BACKEND_KEY = "isolation_session", and it supplies concrete Rust types for each phase's configuration and metadata.

This backend uses the default trait body for provision to mint opaque identifiers, then overrides exec to run processes inside the isolated session. It demonstrates how a backend can participate in the MXC state-aware sandbox lifecycle without retaining complex state in the MXC executor itself.

Error Model and Stateless Enforcement

All failures surface as a typed MxcError carrying a closed ErrorCode enum. Documented variants include malformed_request, stale_id, and not_started. The error object also carries an optional details map for backend-specific diagnostic payloads.

Crucially, MXC holds no persistent state between calls. The executor validates requests solely through the opaque sandboxId supplied by the caller. If a client reuses an identifier that the backend can no longer resolve, the executor returns error.code: "stale_id" and MXC forwards it directly to the SDK user. This stateless design is specified in docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md §4.

Complete TypeScript Example

The following end-to-end example from the design document §6.3 demonstrates a full state-aware flow using the @microsoft/mxc-sdk package. It provisions an isolation_session sandbox, starts it, runs a buffered command, then tears it down.

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

// 1️⃣ Provision – cross-cutting fields are supplied here
const provisionCfg: IsolationSessionProvisionConfig = {
  filesystem: { readwritePaths: ['C:\\workspace'] },
  network: { defaultPolicy: 'allow', allowedHosts: ['api.anthropic.com'] },
};
const opts: SandboxSpawnOptions = { experimental: true };

const { sandboxId } = await provisionSandbox('isolation_session', provisionCfg, opts);

// 2️⃣ Start – backend-specific configuration
await startSandbox(sandboxId, { configurationId: 'small' }, opts);

// 3️⃣ Exec – buffered call (good for short commands)
const execRes = await execInSandboxAsync(
  sandboxId,
  { process: { commandLine: 'echo hello', timeout: 5_000 } },
  opts,
);
console.log(execRes.stdout); // → "hello\n"

// 4️⃣ Stop & 5️⃣ Deprovision when finished
await stopSandbox(sandboxId, {}, opts);
await deprovisionSandbox(sandboxId, {}, opts);

For long-running interactive workloads, the same flow can substitute execInSandboxAsync with the streaming execInSandbox variant to maintain an open PTY. This pattern keeps the sandbox alive across multiple commands without repeating the provision step.

Summary

  • MXC's state-aware sandbox lifecycle splits sandbox management into five independent phases: provision, start, exec, stop, and deprovision.
  • Opaque branded identifiers (SandboxId<C>) encode the backend prefix, enabling stateless routing in src/core/wxc_common/src/state_aware_dispatch.rs.
  • The Rust trait StatefulSandboxBackend in src/core/wxc_common/src/state_aware_backend.rs defines the contract, providing default provision logic and requiring only exec.
  • The TypeScript SDK exposes phase-specific functions in @microsoft/mxc-sdk, serializing requests to JSON and sending them via --config-base64 to the executor.
  • Error handling is strictly typed through MxcError, with variants such as stale_id enforcing correct lifecycle transitions without MXC maintaining session state.

Frequently Asked Questions

What is the difference between MXC's classic spawn and the state-aware sandbox lifecycle?

The classic spawnSandbox* family performs provisioning, execution, and teardown in a single monolithic call. The state-aware API exposes five discrete phases that can be invoked independently, allowing callers to reuse a sandbox across multiple exec invocations and control exactly when resources are allocated and released.

How does MXC route state-aware requests to the correct backend?

The executor inspects the sandboxId prefix—such as iso for the isolation_session backend—to determine routing. This logic is implemented in src/core/wxc_common/src/state_aware_dispatch.rs, and it requires no persistent state table because the backend identifier is embedded directly in the opaque handle.

What happens if a caller uses a stale or invalid sandboxId?

Because MXC itself stores no session state, a reused or expired sandboxId is evaluated by the target backend. When the backend cannot resolve the identifier, it returns an MxcError with code stale_id, which the executor forwards unmodified to the TypeScript SDK caller.

Which phases must a backend implement to support the MXC state-aware lifecycle?

According to the StatefulSandboxBackend trait definition in src/core/wxc_common/src/state_aware_backend.rs, only the exec method is mandatory. The trait provides a default provision implementation that mints random IDs, and backends may leave start, stop, and deprovision as no-ops if their underlying platform does not require explicit state transitions.

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 →