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
43 changes: 39 additions & 4 deletions docs/backends/wslc/wsl-container-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,42 @@ The separate `provision` operation takes the same list under

The lifecycle daemon creates one container per sandbox, so a forward belongs
to the sandbox that declared it. The session-wide `cpuCount` / `memoryMb` /
`gpu` settings remain available only for create-and-run execution, because every
lifecycle `provision` currently joins one shared session. The daemon itself holds
several independently sized sessions; what no caller can yet do is ask for one.
`gpu` settings size the session rather than the sandbox, so a lifecycle caller
asks for them on a separate `provision-session` operation and then names the
returned session on each `provision`:

```json
{
"version": "1.1.0-alpha",
"phase": "provision-session",
"containment": "wslc",
"wslc": {
"provisionSession": { "cpuCount": 4, "memoryMb": 8192 }
}
}
```

```json
{
"version": "1.1.0-alpha",
"phase": "provision",
"containment": "wslc",
"wslc": {
"provision": {
"image": "alpine:latest",
"sessionId": "wslcsess:0123456789abcdef0123456789abcdef"
}
}
}
```

Every sandbox in the session shares that VM, so `cpuCount: 4` means the VM has
four virtual CPUs rather than four per sandbox. Omitting `sessionId` joins an
implicit session shared by every request that names none, which is what a WSLC
`provision` has always done. Release the session with `deprovision-session`
once its sandboxes are gone; it is refused while any remain. See
[the lifecycle guide](wslc-state-aware.md#sessions).

`storagePath` is create-and-run-only
for a different reason: the daemon leases its store from a slot pool rather
than taking a caller-named path, so warming its cache is a separate step. See
Expand Down Expand Up @@ -643,7 +676,9 @@ policy-persistence primitive, so there is nothing for the flag to select.

Container lifecycle JSON requests differ: they reject the whole `lifecycle` section
at parse time, because a multi-invocation sandbox's lifetime is driven by the
explicit `provision` / `deprovision` phases rather than by per-run flags.
explicit `provision` / `deprovision` phases rather than by per-run flags. The
session those sandboxes live in has its own pair, `provision-session` /
`deprovision-session`.

## Supported workloads

Expand Down
170 changes: 141 additions & 29 deletions docs/backends/wslc/wslc-state-aware.md

Large diffs are not rendered by default.

90 changes: 83 additions & 7 deletions docs/containment-policy/dev/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,16 +169,91 @@ operations exist for **IsolationSession, WSLc, and Windows Sandbox** in this
contract. `provision` requires a concrete `containment` and must omit
`sandboxId`; subsequent phases require the ID returned by provision
(`iso:...`, `wslc:...`, or `wsb:...`) and infer the backend from its prefix.
Every phase requires `version`; all admit optional `$schema`, `_comment`,
and `telemetry.enabled`.
WSLc also supports `provision-session` and `deprovision-session`, which require
`containment: "wslc"` and carry no `sandboxId`. Every phase requires `version`;
all admit optional `$schema`, `_comment`, and `telemetry.enabled`.

| Phase | Allowed request fields besides the common fields |
|---|---|
| `provision` / `isolation_session` | Required all-allow `network`; optional `isolationSession.provision.appId` (string). No filesystem, UI, or proxy. |
| `provision` / `wslc` | Optional `filesystem`, `network`, and `wslc.provision.image`, `imageTarPath`, or `portMappings`. Port mappings require all-allow bridged networking; per-container CPU/memory/GPU/storage are not available at provision. |
| `provision-session` / `wslc` | Optional `wslc.provisionSession.cpuCount`, `memoryMb`, and `gpu`; no process, filesystem, network, or UI policy. |
| `provision` / `wslc` | Optional `filesystem`, `network`, and `wslc.provision.image`, `imageTarPath`, `portMappings`, or `sessionId`. Port mappings require all-allow bridged networking; CPU/memory/GPU sizing belongs to the session, not the sandbox. No storage-path override. |
| `provision` / `windows_sandbox` | Optional `filesystem` only. No network, UI, proxy, or backend-specific settings; provision stores the immutable filesystem policy. Requires experimental authorization. |
| `start`, `stop`, `deprovision` | Required `sandboxId`; no process or policy sections. |
| `exec` | Required `sandboxId` and `process` (non-empty `commandLine`); optional `network` and `runtimeConfig` in the JSON shape. All three backends reject supplied network policy at exec and inherit their provision posture. IsolationSession and Windows Sandbox reject a proxy; WSLc accepts an exec proxy. |
| `deprovision-session` / `wslc` | Required `sessionId`; no process, filesystem, network, UI, or backend configuration. |

### WSLc sessions

A session is one WSL2 utility VM shared by the sandboxes placed in it.
`provision-session` sets VM sizing; it does not create a sandbox:

```json
{
"version": "1.1.0-alpha",
"phase": "provision-session",
"containment": "wslc",
"wslc": {
"provisionSession": { "cpuCount": 2, "memoryMb": 4096, "gpu": false }
}
}
```

| Sizing field | Meaning |
|---|---|
| `cpuCount` | Optional non-negative integer, up to `4294967295`; a positive value cannot exceed the host's logical processor count. Omitted or `0` uses the SDK default. |
| `memoryMb` | Optional non-negative integer; the backend rejects values above `4294967295`. A ceiling, not a reservation, so it may exceed installed host RAM. Omitted or `0` uses the SDK default. |
| `gpu` | Optional boolean, default `false`; enables GPU passthrough when supported by the host runtime. |

Success returns `{"result":{"sessionId":"wslcsess:0123456789abcdef0123456789abcdef"}}`.
The ID shown here is illustrative: use the returned value, which has exactly
32 lowercase hexadecimal digits after `wslcsess:`, in later requests.
Treat it as an opaque bearer capability and keep it out of logs.

Place a sandbox in that session with `wslc.provision.sessionId`:

```json
{
"version": "1.1.0-alpha",
"phase": "provision",
"containment": "wslc",
"wslc": {
"provision": {
"image": "alpine:latest",
"sessionId": "wslcsess:0123456789abcdef0123456789abcdef"
}
}
}
```

The VM's CPU, memory, and GPU settings apply to every sandbox in the session,
not as per-sandbox quotas. Omitting `sessionId` joins the implicit shared
session, preserving existing behavior. An isolated provision needs its image
already cached in the selected session's store or supplied as `imageTarPath`.

After deprovisioning every sandbox in the session, release it:

```json
{
"version": "1.1.0-alpha",
"phase": "deprovision-session",
"containment": "wslc",
"sessionId": "wslcsess:0123456789abcdef0123456789abcdef"
}
```

Release returns `{"result":{}}`. A session still holding sandboxes or work
is refused rather than cascading deletion; the implicit session cannot be
released explicitly. Unused sessions can expire before explicit release.
Malformed IDs return `malformed_id`; a valid ID that no longer names a
session returns `not_provisioned`.

A dry run validates either session request without contacting the daemon,
minting an ID, creating a VM or store, or reserving capacity, and returns
`{"result":{}}`. Neither session phase is available through typed SDK V1,
which still targets contract `1.0.0`.

### Sandbox examples

Example WSLc provision with a container-specific TCP port mapping:

Expand Down Expand Up @@ -228,7 +303,8 @@ with the returned one):
}
```

When calling the executor directly, pass routing fields via `--operation` and
`--container-id` instead of putting `phase` and `sandboxId` in the JSON file;
the executor inserts them before parsing. JSON library/FFI calls include
those fields as shown above.
When calling the executor directly, pass `phase` via `--operation`, `sandboxId`
via `--container-id`, and a release's `sessionId` via `--session-id` instead
of putting those fields in the JSON file; the executor inserts them before
parsing. Placement's nested `wslc.provision.sessionId` stays in JSON.
JSON library/FFI calls include routing fields as shown above.
52 changes: 45 additions & 7 deletions docs/development/architecture/container-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,9 @@ Backend-routing fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `containment` | `ContainmentType` or `ContainmentBackend` member | One-shot: yes. State-aware: yes for `provision`, absent for `start` / `exec` / `stop` / `deprovision`. | Backend selection on calls that do not yet have a `sandboxId`. |
| `sandboxId` | branded string | State-aware non-provision: yes. Otherwise absent. | Opaque sandbox id returned by `provision`. Carries the backend prefix used to route non-provision calls (Β§5). |
| `containment` | `ContainmentType` or `ContainmentBackend` member | One-shot: yes. State-aware: yes for `provision` and both session phases, absent for `start` / `exec` / `stop` / `deprovision`. | Backend selection on calls that carry no backend-prefixed `sandboxId`. |
| `sandboxId` | branded string | State-aware sandbox-addressed phases: yes. Otherwise absent. | Opaque sandbox id returned by `provision`. Carries the backend prefix used to route those calls (Β§5). |
| `sessionId` | string | `deprovision-session`: yes. Otherwise absent. | Opaque session id returned by `provision-session`. Carries no routing prefix, which is why its phase declares `containment`. |

State-aware-only fields:

Expand All @@ -456,29 +457,66 @@ State-aware-only fields:
| `phase` | `Phase` member | Yes | Discriminator. Absence means a one-shot request. |
| `process` | `ProcessConfig` | Required for `exec`; absent otherwise. | Cross-backend execution fields. |

#### Raw JSON entry points

Rust's `mxc_sdk::__ffi::run_lifecycle_json` returns the response-envelope JSON
as a string. The C function `mxc_run_state_aware_json` accepts NUL-terminated
UTF-8 request JSON and writes its response to `MxcStateAwareResult`.
Both accept the routing fields shown above and use the declared contract.

| Phase | Successful response |
|---|---|
| `provision` | `result.sandboxId` and any backend metadata |
| `start`, `stop`, `deprovision` | `{"result":{}}` |
| `provision-session` | `result.sessionId`; WSLc contract `1.1.0-alpha` only |
| `deprovision-session` | `{"result":{}}`; WSLc contract `1.1.0-alpha` only |

Session operations are not exposed by typed SDK V1. Their sizing, placement,
and cleanup rules are described in the
[development policy reference](../../containment-policy/dev/policy.md#wslc-sessions).
With `dry_run` enabled, either entry point validates without allocating
resources or minting IDs and returns `{"result":{}}`. A non-dry-run `exec`
uses Rust's `execute_lifecycle_json` or a C exec entry point instead.

The separate `experimental` argument authorizes experimental backends; without
it they fail as `backend_unavailable` before execution. Production backends
ignore this argument. Rust failures return `Error` with the matching
`ErrorCode`; C failures populate the status and error fields, leaving the
response JSON absent.

The C status is both returned and stored in `out->status`. A null `out` returns
`MXC_STATUS_NULL_ARGUMENT` before dispatch. Release every populated result with
`mxc_state_aware_result_free`, including failures.

#### `wxc-exec` lifecycle transport

"MXC request JSON" retains `phase` and `sandboxId`. Direct
"MXC request JSON" retains `phase`, `sandboxId`, and `sessionId`. Direct
`wxc-exec` lifecycle calls instead remove those routing fields from the supplied JSON
and pass them as command-line arguments:

```text
wxc-exec.exe policy.json --operation provision-session
wxc-exec.exe policy.json --operation provision
wxc-exec.exe policy.json --operation start --container-id iso:abc
wxc-exec.exe policy.json --operation exec --container-id iso:abc -- command arg
wxc-exec.exe policy.json --operation stop --container-id iso:abc
wxc-exec.exe policy.json --operation deprovision --container-id iso:abc
wxc-exec.exe policy.json --operation deprovision-session --session-id wslcsess:0123456789abcdef0123456789abcdef
```

Provision JSON still contains `containment`; later operations route from the
`--container-id` prefix. Supplying `phase` or `sandboxId` in CLI JSON is rejected rather
Replace the illustrative session ID with `result.sessionId` from a prior
`provision-session` response; a correctly formatted ID must also name a live session.

Provision and session JSON still contains `containment`; sandbox-addressed operations route from the
`--container-id` prefix. Supplying `phase`, `sandboxId`, or `sessionId` in CLI JSON is rejected rather
than overriding the command-line routing. Without `--operation`, `wxc-exec` accepts
only one-shot JSON.

`--operation` cannot be combined with delete or maintenance routing, including
`--delete`, its `--containername` option, setup modes, probing, force-reclaim, or
audit modes. Lifecycle identity comes only from `--container-id`; conflicting options
are rejected rather than ignored.
audit modes. Lifecycle identity comes only from `--container-id` or `--session-id`,
which are mutually exclusive; an operation supplied the wrong one is told which it
wanted. Conflicting options are rejected rather than ignored.

Cross-cutting fields available to state-aware (state-aware-only at top level β€” backends
declare which phases honor them, see Β§10.3):
Expand Down
47 changes: 23 additions & 24 deletions docs/development/architecture/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ the one-shot path, a clean non-zero sandbox exit is not treated as an MXC error.
| `mxc.outcome` | string | `"success"` or `"failure"` |
| `mxc.duration_ms` | uint64 | Total execution time |
| `mxc.failure_reason` | string | Failure category (if applicable) |
| `mxc.phase` | string | State-aware lifecycle phase (`provision`\|`start`\|`exec`\|`stop`\|`deprovision`); empty for one-shot executions |
| `mxc.phase` | string | Lifecycle phase (`provision-session`\|`provision`\|`start`\|`exec`\|`stop`\|`deprovision`\|`deprovision-session`); empty for create-and-run execution |
| `__TlgCV__` | string | Microsoft Correlation Vector (MS-CV) β€” the lifecycle correlation key (see [Correlating a lifecycle](#correlating-a-lifecycle)); empty for one-shot executions |

### MXC.Error
Expand Down Expand Up @@ -283,19 +283,19 @@ MS-CV is a hierarchical, propagatable identifier of the form
`<base>.<element>.<element>…` β€” a random base (128 bits, 22 base64 chars) plus a
dotted chain of decimal elements. MXC uses it as follows:

- **`provision`** seeds a fresh random base (`correlation_vector::seed()`) and
returns it to the client in its result envelope as `result.correlationVector`.
This is the lifecycle's root vector.
- The client **relays** that vector verbatim into every later phase (the SDK
surfaces it as `ProvisionResult.correlationVector` and accepts it back as
`SandboxSpawnOptions.correlationVector`, sent on the wire as the top-level
`correlationVector` field).
- Each non-`provision` phase **spins** the relayed base
(`correlation_vector::spin()`) to derive a distinct child vector that still
shares the lifecycle's base prefix. Spin (rather than a plain extend) is used
because `exec` is multi-invocation and the client is a dumb relay β€” a plain
extend would collapse every phase to the same `base.0`, whereas spin folds in a
coarse timestamp + entropy so sibling phases get distinct, ordered vectors.
- **`provision`** seeds a fresh random base and persists it locally once the
sandbox ID is known.
- **`start`, `exec`, `stop`, and `deprovision`** recall that persisted root and
spin distinct child vectors sharing its base prefix. Successful deprovision
removes the record; a dry run keeps it.
- Callers neither receive nor relay a correlation vector. It is internal to
MXC, not a JSON request or response field.

`provision-session` and `deprovision-session` each seed an independent vector
when telemetry is authorized. They do not read, create, refresh, sweep, or
delete the sandbox correlation store. Session IDs are not correlation-store
keys or vector seeds. This applies to the executor, raw JSON entry points,
and typed engine calls. Sandbox operations retain their existing store cleanup.

An analyst groups all phases of one lifecycle by the shared **base prefix** of
`__TlgCV__`, and orders/​distinguishes phases within it by the spun elements.
Expand All @@ -307,11 +307,11 @@ element boundary, if needed to stay within the cap β€” never truncating a
multi-digit element to a falsified value like `.42` β†’ `.4`) and the vector is
never mutated again. `increment` likewise freezes when its trailing element is
already at `u32::MAX` and so cannot advance (the spec's value-overflow
behaviour). Every later phase then relays and emits that same frozen vector
behaviour). Every later sandbox phase then emits that same frozen vector
verbatim, so correlation by base prefix still holds β€” only the per-phase
ordering elements stop advancing.

**Canonical validation.** A relayed vector is only built on (spun) when it is
**Canonical validation.** A persisted vector is only built on (spun) when it is
*relayable* β€” a well-formed mutable vector (canonical 22-char base64 base whose
final char encodes a zero low nibble, followed by one or more canonical decimal
`u32` elements) or a valid frozen (`!`-terminated) vector. Elements must be
Expand All @@ -325,15 +325,15 @@ base rather than emitted verbatim.
The correlation vector is **not** derived from the `sandbox_id`, so no
caller-supplied identity (e.g. a UPN embedded in an IsolationSession
`iso:<upn>` id) is ever involved: the base is pure randomness. Spin is defensive
β€” a missing, empty, or malformed relayed value falls back to a fresh seed rather
β€” a missing, empty, or malformed stored value falls back to a fresh seed rather
than panicking telemetry. `__TlgCV__` is empty for one-shot executions (which have
no lifecycle to correlate) and for a crash during `provision` before the vector is
stashed. It is only computed and emitted when experimental telemetry is active, so
provision output is unchanged when telemetry is off.
stashed. It is only computed and emitted when telemetry is authorized; response
envelopes never contain it.

**Why a relayed random vector rather than a hashed `sandbox_id`.** An alternative
**Why a random vector rather than a hashed `sandbox_id`.** An alternative
design would derive the correlation key deterministically from the `sandbox_id`
(e.g. a hash), avoiding the wire/SDK relay entirely. We deliberately do **not** do
(e.g. a hash), avoiding persistence of a random root. We deliberately do **not** do
this, for two reasons:

1. **PII-safety.** A `sandbox_id` is caller-influenced and can embed identity β€” an
Expand All @@ -349,9 +349,8 @@ this, for two reasons:
the hierarchical parent/child structure (base prefix + spun elements) that lets
an analyst reconstruct phase ordering, not just group-by a flat key.

The client relay is therefore a required part of the design, not incidental
plumbing: it is how the random root vector minted at `provision` reaches the later
per-phase executor processes, which otherwise share no state.
Local persistence lets the random root minted at `provision` reach later
executor processes without exposing it to callers.

### Crash telemetry (panic hook)

Expand Down
9 changes: 6 additions & 3 deletions docs/development/architecture/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,12 @@ reasons:
Raw SDK entry points enforce the same exact set. High-level v1 one-shot and
state-aware APIs select stable `1.0.0` internally and expose only the
backends supported by that contract. Capabilities the published contract does
not declare β€” Windows Sandbox state-aware, and
`wslc.provision.portMappings` β€” are reachable through the raw `1.1.0-alpha`
path only. The compatibility constants in
not declare β€” Windows Sandbox state-aware, `wslc.provision.portMappings`,
and WSLc session operations (`provision-session`, `deprovision-session`,
and `wslc.provision.sessionId`) β€” require MXC request JSON declaring
`1.1.0-alpha`, or the in-process engine with that contract.
`stateAwareWslc` names this development contract; it does not change the
published `1.0.0` target of the V1 SDKs. The compatibility constants in
`schemas/schema-version.json` do not authorize other versions within their
minimum/maximum range.
- **Product version** tracks the shipped artifacts and moves independently of the
Expand Down
Loading
Loading