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

> Explore MXC's state-aware sandbox lifecycle with its 5-phase architecture. Learn how provision, start, exec, stop, and deprovision offer granular control for efficient sandbox management.

- Repository: [Microsoft/mxc](https://github.com/microsoft/mxc)
- Tags: architecture
- Published: 2026-06-07

---

**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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/sdk/src/types.ts) and operating on the branded `sandboxId`.

```typescript
// 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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/src/core/wxc_common/src/state_aware_backend.rs).

```rust
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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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.

```typescript
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`](https://github.com/microsoft/mxc/blob/main/src/core/wxc_common/src/state_aware_dispatch.rs).
- **The Rust trait `StatefulSandboxBackend`** in [`src/core/wxc_common/src/state_aware_backend.rs`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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`](https://github.com/microsoft/mxc/blob/main/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.