Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions docs/isolation-session/state-aware-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,64 @@ wire-format `MxcError` codes via `map_lifecycle_error`:
| `IsolationSessionError` variant | Wire `error.code` | Trigger |
|---|---|---|
| `Policy(...)` | `policy_validation` | Caller-supplied policy field that this phase does not accept β€” see the honor matrix above. Rejected by `validate_<phase>` hooks (state-aware) or `validate_runner` (one-shot). |
| `ServiceUnavailable(...)` | `backend_unavailable` | `IsoSessionOps` activation failure: the `Windows.AI.IsolationSession.Preview` API is unavailable on this OS build (not registered, or the OS feature gate is off). HRESULTs `CLASS_E_CLASSNOTAVAILABLE` (`0x80040111`) or `REGDB_E_CLASSNOTREG` (`0x80040154`). |
| `Stale(...)` | `stale_id` | OS-side `AgentManager::FindActiveAgentUserByProvisionId` returns `HRESULT_FROM_WIN32(ERROR_NOT_FOUND)` (`0x80070490`) β€” the `provisionId` is missing from both the in-memory cache and the persisted registry. After `deprovision`, every non-provision op against the dead `sandboxId` triggers this. |
| `Lifecycle(...)` | `backend_error` | Any other HRESULT from a lifecycle op. The error message embeds the operation name, HRESULT, OS-side message, and remediation hint where present. |
| `ServiceUnavailable(...)` | `backend_unavailable` | Activation failure of the in-proc IsolationSession runtime API: it is unavailable on this OS build (not registered, or the OS feature gate is off). HRESULTs `CLASS_E_CLASSNOTAVAILABLE` (`0x80040111`) or `REGDB_E_CLASSNOTREG` (`0x80040154`). |
| `Stale(...)` | `stale_id` | The OS service reports `HRESULT_FROM_WIN32(ERROR_NOT_FOUND)` (`0x80070490`) β€” the agent user is unknown to it. After `deprovision`, every non-provision op against the dead `sandboxId` triggers this. |
| `Lifecycle(...)` | `backend_error` | Any other failure of a lifecycle op, whether the API reported it semantically or the call itself could not be completed. |

`error.details` is empty in v1. The HRESULT and OS-side message live inside
`error.message` rather than as a structured field.
### Structured failure fields

The components of an API failure travel as **discrete fields** on the wire error
envelope β€” `operation`, `nativeCode` and `remediation` β€” rather than being concatenated
into `message`. `message` holds the bare human-readable text; for a semantic API failure
that is the API's own message, passed through verbatim.

| Failure | `operation` | `nativeCode` | `remediation` |
|---|---|---|---|
| Semantic API failure (the call completed and reported an error) | βœ… | βœ… | when the API supplies one |
| Transport failure (the call could not be completed, or a result property could not be read) | βœ… | βœ… | β€” |
| Activation failure (`backend_unavailable`) | βœ… | βœ… | β€” |
| The API's status code itself could not be read | βœ… | β€” | best-effort |
| MXC-internal failure (relay threads, console handles) | β€” | β€” | β€” |
| `Policy` and the MXC-side `malformed_*` rejections | β€” | β€” | β€” |

**Invariant:** `nativeCode` implies `operation`, and `remediation` implies `operation`.
`operation` marks that an API operation was in flight; neither refinement appears alone.

`operation` is the interface-qualified member name β€” for example
`IsoSessionOps.StopSessionAsync`. It is deliberately low-cardinality and free of call
parameters (a failing environment-variable insert names the variable in `message`, not
in `operation`) so that consumers can aggregate on it. Where a lifecycle call succeeds
but reading one of its result properties fails, `operation` stays the lifecycle call and
the finer step is described in `message`.

These values are **best-effort diagnostics, not a versioned contract**: they mirror the
projected WinRT class and method names, which this repo does not own. Branch on `code`;
treat `operation` as telemetry and log detail. See the
[cross-backend contract](../state-aware-lifecycle/mxc-state-aware-sandbox-api.md) Β§7.3.

`nativeCode` is the HRESULT rendered as lowercase hex, e.g. `0x80070490`.

`message` is the API's own text, passed through verbatim, and is never empty: when the
API reports a failure without a message, a short stand-in is substituted, because the
operation and status now live in their own fields and no longer backfill it.

`error.details` is unused by this backend. It remains the escape hatch for
backend-specific structured data that has no cross-backend meaning; the three named
fields above are backend-neutral and so live on the envelope itself.

### The `stale_id` promotion is semantic-path only

`ERROR_NOT_FOUND` is promoted to `stale_id` **only** when it arrives through the API's
semantic error channel, and **only** for non-provision operations.

- *Semantic only:* the in-proc client maps its internal codes to standard HRESULTs when
it builds the error object, and that mapping is what gives `0x80070490` the meaning
"agent user not provisioned". The same value arriving as a transport failure has no
such provenance β€” it could be any "not found" from activation or RPC β€” so promoting it
would emit a false `stale_id`, whose remediation is "re-provision; treat the id as
dead", and destroy a healthy sandbox.
- *Non-provision only:* provision mints the agent user. There is no `sandboxId` yet, so
reporting a stale one would be incoherent.

## Cancellation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ on the response, and neither shape carries `containerId`.
| TypeScript SDK (reference Β§6) | Five new functions: `provisionSandbox`, `startSandbox`, `execInSandbox` / `execInSandboxAsync`, `stopSandbox`, `deprovisionSandbox`. Branded `SandboxId<C>` type tagging ids by backend (`containment` named once at provision, inferred from the id thereafter). Per-(backend, phase) typed `*Config` interfaces (e.g. `IsolationSessionProvisionConfig`) that absorb cross-cutting fields directly β€” no separate policy parameter. Per-phase typed `*Result` types per backend. `AbortSignal` cancellation via the existing `SandboxSpawnOptions`. Typed `MxcError` class carrying a closed-enum `code`. | `spawnSandbox` family preserved. `ContainmentBackend` extension reused. The wire-format-aligned `Process` / `Filesystem` / `Network` / `UiConfig` interfaces from `sdk/node/src/types.ts` are reused as field types inside state-aware Configs. `SandboxSpawnOptions` reused as the third-arg options bag (gains `signal?: AbortSignal`). `*Config` naming convention reused. |
| JSON wire format (reference Β§7) | Top-level `phase` discriminator. Top-level `sandboxId`. `containment` carried on provision only; non-provision phases route via the `sandboxId` prefix. Per-phase nesting under `experimental.<backend>.<phase>`. Named envelope types as a TypeScript discriminated union. | One-shot configs (no `phase`) work unchanged. Cross-cutting `filesystem` / `network` / `ui` at top level for state-aware too β€” backends declare per-phase honor. |
| Rust executor (reference Β§9) | Dispatch arm for state-aware. New `StatefulSandboxBackend` trait. Rust mirror of the wire envelope (the `wire::MxcConfig` parse target). | `ScriptRunner` trait. Existing one-shot dispatch path. Existing backends unchanged. |
| Error model (reference Β§8) | Closed enum of 12 codes. `MxcError` class with `code: ErrorCode`. `details` open object. | Existing one-shot error paths preserved. |
| Error model (reference Β§8) | Closed enum of 12 codes. `MxcError` class with `code: ErrorCode`. Named structured fields `operation` / `nativeCode` / `remediation` for failures raised by an underlying platform API, plus the open `details` object for backend-specific data. | Existing one-shot error paths preserved. |
| Plug-in surface (reference Β§11) | Implement `StatefulSandboxBackend`. Define typed per-(backend, phase) `*Config` interfaces. Declare the trait's `ID_PREFIX` and `BACKEND_KEY` consts. Document the cross-cutting honor matrix. | Ephemeral-only backends require no changes. |

## Lifecycle
Expand Down Expand Up @@ -144,7 +144,8 @@ The wire envelope is a TypeScript discriminated union over `phase`, JSON-seriali
The Rust executor parses the same shape into the typed wire model
(`wire::MxcConfig`, reference Β§9.1). The only `Record<string, unknown>` in the contract is
`ErrorEnvelope.details` β€” the escape hatch for backend-specific structured failure
information.
information. Backend-neutral failure detail travels in the error envelope's named
fields (`operation`, `nativeCode`, `remediation`) instead.

```typescript
interface OneShotRequest {
Expand Down Expand Up @@ -374,8 +375,11 @@ rejected.

## Error codes

Closed enum at the MXC layer; backend-specific failures use `backend_error` with
structured `details`. Reference Β§8 has the full list and the `MxcError` mapping.
Closed enum at the MXC layer; backend-specific failures use `backend_error`, with the
detail carried in the error envelope's named structured fields (`operation`,
`nativeCode`, `remediation`) and, where a backend needs data with no cross-backend
meaning, in `details`. Reference Β§8 has the full list and the `MxcError` mapping, and
Β§7.3 the field invariant.

| Group | Codes |
|---|---|
Expand All @@ -384,7 +388,7 @@ structured `details`. Reference Β§8 has the full list and the `MxcError` mapping
| Id problems | `malformed_id`, `stale_id` |
| State-machine violations | `not_provisioned`, `not_started`, `already_started`, `already_stopped` |
| Config / policy | `policy_validation` |
| Catch-all | `backend_error` (with structured `details`) |
| Catch-all | `backend_error` |

Process-runtime kill conditions (timeouts, backend-initiated termination) surface as
sentinel exit codes from the exec process, not as typed wire-format errors. Each code
Expand Down
47 changes: 43 additions & 4 deletions docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@ The wire contract is a typed envelope, JSON-serialised, that flows from the SDK
executor (`wxc-exec` on Windows, `lxc-exec` on Linux) via the existing `--config-base64`
CLI argument. Both ends agree on the same shape: the SDK serialises a TypeScript value,
the executor parses the same value into a Rust struct (Β§9.1). The only open content in
the envelope is at the leaves of `ErrorEnvelope.details`.
the envelope is at the leaves of `ErrorEnvelope.details`; every other field, including
the error envelope's named structured fields, is statically typed.

### 7.1 Request envelope

Expand Down Expand Up @@ -750,6 +751,9 @@ output to a file and leaves stderr as pure script content.
interface ErrorEnvelope {
code: ErrorCode;
message: string;
operation?: string;
nativeCode?: string;
remediation?: string;
details?: Record<string, unknown>;
}

Expand Down Expand Up @@ -778,10 +782,45 @@ Because MXC diagnostic output is routed to `stderr` in state-aware mode, this
stdout-based discrimination has no false positives or negatives β€” the content is always
either pure envelope or pure script output.

`code` and `message` are always present. `code` is the machine-readable category a
consumer branches on; `message` is the human-readable description, and for a failure
raised by an underlying platform API it is that API's own message, passed through
verbatim rather than concatenated with the other fields.

The three optional named fields describe a failure that originated in an underlying
Comment thread
adpa-ms marked this conversation as resolved.
platform API:

| Field | Meaning |
|---|---|
| `operation` | The API call that failed, namespaced by its interface β€” e.g. `IsoSessionOps.RunProcessWithOptionsAsync`. Low-cardinality and free of call parameters, so it is safe to aggregate on in telemetry. **Best-effort diagnostic, not a versioned contract** β€” see below. |
| `nativeCode` | The underlying platform status as a string. An HRESULT such as `0x80070490` on Windows; the field is platform-neutral, so another backend can carry an errno or equivalent. |
| `remediation` | The API's actionable "how to fix it" hint, when it supplies one. |

**Availability.** These fields 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 composes its full detail into `message` instead, so all
three are uniformly absent there. Other backends may adopt them as they grow an
equivalent channel β€” treat all three as optional on every backend, and branch program
logic on `code` first.

**Stability.** Unlike `code`, which is a closed and versioned enum, the *values* of `operation` and `nativeCode` are **best-effort diagnostics and may change without a schema version bump**. They are derived from the underlying platform API β€” for IsolationSession, from the projected WinRT class and method names β€” which MXC does not own and cannot version. Consumers should aggregate on them for telemetry and log them for diagnosis, but branch program logic on `code`, and should not treat a particular `operation` value as a guarantee. (MXC's own end-to-end tests do pin exact values; that is deliberate β€” they verify MXC's mapping, and move with it in the same change.)

**Invariant:** `nativeCode` implies `operation`, and `remediation` implies `operation`.
`operation` marks that an API operation was in flight; the other two refine it, and
neither ever appears alone. A failure MXC raises before or outside any API call β€” a
malformed request or id, a policy rejection, or an internal failure of MXC's own
machinery β€” carries only `code` and `message`.

**Which fields earn a place here.** A named top-level field is for a **backend-neutral**
concept: `operation`, `nativeCode` and `remediation` all apply equally to a Windows
HRESULT, a Linux errno, or any other backend's failure. **Backend-specific** structured
data belongs in `details` instead. That is what keeps `details` from becoming vestigial
as named fields are added β€” it remains the designated home for anything without a
cross-backend meaning.

`ErrorEnvelope.details` is the only `Record<string, unknown>` in the contract. It's the
escape hatch backends use to convey structured failure information that's
per-error-code (a backend's native HRESULT, partial output captured before a timeout,
etc.). Each backend's plan doc (Β§11) specifies what `details` contains for which error
escape hatch backends use to convey structured failure information that has no dedicated
field. Each backend's plan doc (Β§11) specifies what `details` contains for which error
codes.

### 7.4 Worked example: IsolationSession end-to-end
Expand Down
27 changes: 26 additions & 1 deletion sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,30 @@ 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`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.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:

```typescript
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 and travel together: `nativeCode` and `remediation` never appear without `operation`. 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/`](https://github.com/microsoft/mxc/tree/main/docs/state-aware-lifecycle/).

</details>
Expand Down Expand Up @@ -391,7 +415,8 @@ getTemporaryFilesPolicy(env?) β†’ FilesystemPolicyResult
UiCapabilitySupport

// Errors (typed wire-format errors from wxc-exec)
ErrorCode, MxcError, mxcErrorFromCode(code)
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`.
Expand Down
Loading
Loading