Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the `
| Windows Sandbox | `wxc-exec.exe` | Windows | `backends/windows_sandbox/lifecycle/src/` (live transient one-shot `WindowsSandboxRunner` + state-aware `StatefulSandboxBackend`). Experimental β€” requires `--experimental`. Supports both **one-shot** (a fresh, disposable VM per invocation with guaranteed teardown, via `ScriptRunner`) and **state-aware** (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. State-aware holds a single live VM across separate `wxc-exec` phase processes behind a persistent detached host-side daemon (`backends/windows_sandbox/daemon/`); the OS enforces a single running Windows Sandbox VM per host, so the daemon owns it and reclaims an orphaned VM on restart only via positive process-identity proof. The shared boot sequence (write per-launch nonce, launch VM, capture ownership proof, wait rendezvous, connect) lives in `backends/windows_sandbox/lifecycle/src/vm.rs::launch_managed_vm`; each mode plugs in its own `LaunchObserver` for the per-caller ownership / proof bookkeeping. Honors `readwritePaths`/`readonlyPaths`/`deniedPaths` (HOST paths) at provision via `.wsb` `<MappedFolder>` entries (mapped at the same absolute host path inside the guest; rejects `deniedPaths` equal-to or nested-within a mapped share since `.wsb` has no Deny primitive); filesystem policy is immutable post-provision. Network isolation is enforced unconditionally by the in-guest agent; `network`/`ui` are not honored. ID prefix `wsb` (strict `wsb:<8-hex>` grammar). Per-launch handshake: 32-byte `Nonce` + 1-byte `ChannelRole` tag on every TCP connection (boot + reconnect); the guest pairs accepted sockets by declared role, not by accept order. The guest agent binary `wxc-windows-sandbox-guest.exe` (`backends/windows_sandbox/guest/`) is injected into the VM. |
| MicroVM (NanVix) | `wxc-exec.exe` | Windows | `backends/nanvix/runner/src/lib.rs` β€” feature-gated behind `microvm` |
| Hyperlight | `wxc-exec.exe` | Windows | `backends/hyperlight/common/src/lib.rs` β€” Hyperlight + Unikraft micro-VM backend |
| IsolationSession | `wxc-exec.exe` | Windows | `backends/isolation_session/common/src/` β€” feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API. Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Rejects all filesystem policy (`readwritePaths`/`readonlyPaths`/`deniedPaths`) at every phase with `policy_validation` β€” the backend has no host-folder-sharing primitive. Likewise rejects any supplied `ui` policy at every phase on both surfaces (as `policy_validation` on the state-aware surface; one-shot discards the typed variant and surfaces `backend_error` with the reason in the message): the isolation session isolates the *host's* UI from contained code but does not deny it UI capabilities (window creation, GDI and the session's own clipboard all work inside it), so no `ui` posture is truthful here β€” there is no value combination that could be accepted instead, which is why there is no acknowledgment-style gate as there is for `network`. The check is presence-based via `ContainerPolicy::ui_specified` (twin of `network_specified`) because `UiPolicy`'s defaults are full lockdown, making an explicit lockdown `ui` indistinguishable by value from an absent one. An omitted `ui` is accepted and applies no restriction β€” the schema's default-deny reading does not hold on this backend. One-shot additionally rejects `lifecycle.destroyOnExit=false` and `lifecycle.preservePolicy=true` β€” the in-proc API has no session-lifetime knob, and the default `destroyOnExit=true` matches actual behavior so it is accepted; the state-aware parser already rejects the whole `lifecycle` section. The full per-phase honor matrix for both surfaces is in `docs/isolation-session/state-aware-rust.md`. The container's network is unrestricted (outbound open; a process inside can listen on a localhost-reachable port) and MXC has no primitive to filter or deny it, so provision (and one-shot) accept ONLY the canonical unrestricted-network acknowledgment β€” `network.defaultPolicy=allow` + `network.allowLocalNetwork=true`, no host rules, no proxy, default enforcement β€” and refuse anything else (including an absent policy, which defaults to the unenforceable deny) with `policy_validation`; post-provision phases reject any supplied network policy (fixed at provision, tracked via `ExecutionRequest.network_specified`) and inherit an absent one. State-aware provision accepts an optional `appId` (the Package Family Name for a packaged app), carried verbatim inside the returned `sandboxId`; the one-shot surface takes no backend configuration at all (a stray `experimental.isolation_session` payload is accepted and ignored). Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. |
| IsolationSession | `wxc-exec.exe` | Windows | `backends/isolation_session/common/src/` β€” feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API. Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Rejects all filesystem policy (`readwritePaths`/`readonlyPaths`/`deniedPaths`) at every phase with `policy_validation` β€” the backend has no host-folder-sharing primitive. Likewise rejects any supplied `ui` policy at every phase on both surfaces (as `policy_validation` on the state-aware surface; one-shot discards the typed variant and surfaces `backend_error` with the reason in the message): the isolation session isolates the *host's* UI from contained code but does not deny it UI capabilities (window creation, GDI and the session's own clipboard all work inside it), so no `ui` posture is truthful here β€” there is no value combination that could be accepted instead, which is why there is no acknowledgment-style gate as there is for `network`. The check is presence-based via `ContainerPolicy::ui_specified` (twin of `network_specified`) because `UiPolicy`'s defaults are full lockdown, making an explicit lockdown `ui` indistinguishable by value from an absent one. An omitted `ui` is accepted and applies no restriction β€” the schema's default-deny reading does not hold on this backend. One-shot additionally rejects `lifecycle.destroyOnExit=false` and `lifecycle.preservePolicy=true` β€” the in-proc API has no session-lifetime knob, and the default `destroyOnExit=true` matches actual behavior so it is accepted; the state-aware parser already rejects the whole `lifecycle` section. The full per-phase honor matrix for both surfaces is in `docs/isolation-session/state-aware-rust.md`. The container's network is unrestricted (outbound open; a process inside can listen on a localhost-reachable port) and MXC has no primitive to filter or deny it, so provision (and one-shot) accept ONLY the canonical unrestricted-network acknowledgment β€” `network.defaultPolicy=allow` + `network.allowLocalNetwork=true`, no host rules, no proxy, default enforcement β€” and refuse anything else (including an absent policy, which defaults to the unenforceable deny) with `policy_validation`; post-provision phases reject any supplied network policy (fixed at provision, tracked via `ExecutionRequest.network_specified`) and inherit an absent one. State-aware provision accepts an optional `appId` (the Package Family Name for a packaged app); it is resolved at provision β€” a non-empty value is used verbatim, while an empty or absent value auto-detects the calling process's PFN and becomes `PFN:<pfn>` (or is kept as-is when unpackaged) β€” then passed to `IsoSessionOps::AddUserAsync2` and carried inside the returned `sandboxId`. The one-shot surface takes no backend configuration at all (a stray `experimental.isolation_session` payload is accepted and ignored) but performs the same default PFN auto-detection. Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. |
| LXC | `lxc-exec` | Linux | `core/lxc/src/main.rs` + `backends/lxc/common/` |
| Seatbelt | `mxc-exec-mac` | macOS | `core/mxc_darwin/src/main.rs` + `backends/seatbelt/common/` β€” uses macOS App Sandbox (Seatbelt) profiles for process containment. Requires schema `0.7.0-alpha`+. Supports `network.proxy` via the same cooperative env-var model as Bubblewrap (injects `HTTP_PROXY`/`HTTPS_PROXY` into the sandbox, reusing `wxc_common::unix_proxy_coordinator`; `builtinTestServer` spawns the shared `unix-test-proxy`). See `docs/macos-support/seatbelt-backend.md`. |
| Bubblewrap | `lxc-exec` | Linux | `backends/bubblewrap/common/src/bwrap_runner.rs` β€” unprivileged sandboxing via Linux user namespaces and `bwrap`. Experimental β€” requires `--experimental`. Uses shared filesystem/network policy fields; per-host network filtering via `NetworkIptablesManager` from `backends/lxc/common`. See `docs/bwrap-support/bubblewrap-backend.md`. |
Expand Down
8 changes: 4 additions & 4 deletions docs/isolation-session/state-aware-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ without metadata use `()`.

| Field | Type | Default | Description |
|---|---|---|---|
| `appId` | string \| absent | absent | Optional identifier for the calling application. For a **packaged** application this is the Package Family Name; for an unpackaged one it may be any string. Carried verbatim inside the `sandboxId` (see below) so later phases recover it without the caller re-supplying it. **Nothing consumes it today** β€” it is accepted now so a future OS contract that acts on the calling application's identity does not require a breaking change. Validated **structurally only** (no control characters; at most 256 characters) β€” MXC is a pass-through carrier here and does not judge what a valid application identity looks like, so enforcing a PFN grammar would risk rejecting forms a future OS API accepts. Preserved verbatim: no trimming, no case folding, no normalisation. An explicitly-supplied **empty string is a distinct value from absent** and round-trips as such (a future OS API may assign it meaning, and MXC never synthesizes an empty string the caller did not send); JSON `null` is a second spelling of absent. Rejections surface as `policy_validation` from `validate_provision`, before any OS call. The wire path is `experimental.isolation_session.provision.appId`. |
| `appId` | string \| absent | absent | Optional identifier for the calling application, associating the provisioned agent user with its owning app. **A packaged application must supply its Package Family Name in the form `PFN:<packageFamilyName>`** (e.g. `PFN:Contoso.App_8wekyb3d8bbwe`) β€” the literal `PFN:` prefix followed by the PFN. Because a non-empty value is used **verbatim**, a packaged caller that passes a bare PFN *without* the `PFN:` prefix will **not** get PFN-scoped registration. An unpackaged application may pass any string. **Resolution at provision:** a non-empty value is used verbatim; an **empty string or absent** value opts into best-effort PFN auto-detection β€” MXC reads the calling (`wxc-exec`) process's Package Family Name and substitutes `PFN:<pfn>`, or, when the caller is unpackaged (or the lookup fails), keeps the original value unchanged (`""` stays `""`, absent stays absent). So a packaged caller can either pass `PFN:<pfn>` explicitly or leave it empty/absent to have MXC apply the `PFN:` prefix for it. The **resolved** value is what is passed to `IsoSessionOps::AddUserAsync2` and carried inside the `sandboxId` (see below), so later phases recover the identity actually used without the caller re-supplying it. **Host gating:** the app-scoped `AddUserAsync2` overload is used only when the host advertises `IsoSessionFeature::AppScopedRegistration` via `GetFeatureLevel` (a positive level); on older hosts β€” where `GetFeatureLevel` returns a non-positive level or rejects the unknown feature value with an error β€” MXC falls back to the legacy `AddUserAsync`, which carries no `appId`, so the agent user is provisioned without app association (the resolved value is still recorded in the `sandboxId`). Validated **structurally only** (no control characters; at most 256 characters) β€” MXC does not judge what a valid application identity looks like, so enforcing a PFN grammar would risk rejecting forms a future OS API accepts. Beyond PFN substitution there is no trimming, case folding, or normalisation. An explicitly-supplied **empty string is a distinct value from absent** on an unpackaged host and round-trips as such; JSON `null` is a second spelling of absent. Rejections surface as `policy_validation` from `validate_provision`, before any OS call. The wire path is `experimental.isolation_session.provision.appId`. |

**Metadata (`IsolationSessionProvisionMetadata`):**

| Field | Type | Description |
|---|---|---|
| `agentUserName` | string | The OS-assigned agent account name returned by `AddUserAsync`, also carried inside the `sandboxId` payload where it serves as the addressing key for every post-provision phase. Format is OS-internal and not stable across builds. |
| `agentUserSid` | string | The security identifier (SID) of the agent user, returned by `AddUserAsync`. Diagnostic only. |
| `agentUserName` | string | The OS-assigned agent account name returned by `AddUserAsync2`, also carried inside the `sandboxId` payload where it serves as the addressing key for every post-provision phase. Format is OS-internal and not stable across builds. |
| `agentUserSid` | string | The security identifier (SID) of the agent user, returned by `AddUserAsync2`. Diagnostic only. |
| `ephemeralWorkspacePath` | string | A directory shared between the calling user and this isolated agent user, through which the caller can stage files into the session. Each isolated user can access only its own workspace; the caller can access every concurrent sandbox's workspace. Created at provision and deleted when the sandbox is deprovisioned. It does **not** change the workload's working directory. |

`appId` is deliberately **not** echoed in the metadata β€” the caller supplied
Expand Down Expand Up @@ -353,7 +353,7 @@ whole section for every backend. See the matrix notes above.

### Multiple sandboxes

Distinct `sandboxId`s map to distinct OS agent users (each `AddUserAsync`
Distinct `sandboxId`s map to distinct OS agent users (each `AddUserAsync2`
mints a fresh account). There is no shared registration between them, so
concurrent provisions are independent and all succeed.

Expand Down
2 changes: 1 addition & 1 deletion docs/isolation-session/state-aware-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ contract (including fields not yet exposed via the SDK).
|---|---|---|---|
| `version` | string | SDK `SUPPORTED_VERSION` | Schema-version override. |
| `network` | `{ defaultPolicy: 'allow'; allowLocalNetwork: true }` | β€” (**required**) | Unrestricted-network acknowledgment. The container runs on a network MXC cannot filter or deny (outbound open; a process inside can listen on a port reachable from outside via localhost), so the caller must explicitly acknowledge it. This exact value is the only one accepted; any other network policy (or omission) is rejected at provision, and `network` is not accepted on the post-provision phases (the posture is fixed at provision). |
| `appId` | string | absent | Optional identifier for the calling application β€” the Package Family Name for a packaged app, any string otherwise. MXC neither interprets nor verifies it: it is carried verbatim inside the returned `SandboxId` so later phases recover it without the caller re-supplying it. Nothing consumes it yet; it is accepted now so a future OS contract acting on the calling application's identity needs no breaking change. Validated structurally only (no control characters, at most 256 characters); rejections surface as `MxcError` with `code: 'policy_validation'`. Whitespace and case are preserved exactly, and an explicitly supplied empty string is a **distinct** value from omitting the field. Provision-phase only β€” it is fixed for the sandbox's lifetime, and the `IsolationSessionStartConfig` type rejects it at compile time. |
| `appId` | string | absent | Optional identifier for the calling application, associating the provisioned agent user with its owning app. **A packaged app must supply its Package Family Name in the form `PFN:<packageFamilyName>`** (e.g. `PFN:Contoso.App_8wekyb3d8bbwe`) β€” the literal `PFN:` prefix followed by the PFN. Because a non-empty value is used **verbatim**, a bare PFN *without* the `PFN:` prefix will **not** be treated as PFN-scoped. An unpackaged app may pass any string. **Resolved at provision by the native layer:** a non-empty value is used verbatim; an empty string (or omitting the field) opts into best-effort PFN auto-detection, where the calling process's Package Family Name becomes `PFN:<pfn>` (or the original value is kept when the caller is unpackaged) β€” so a packaged caller can either pass `PFN:<pfn>` explicitly or appId empty to have MXC attempt to detect the PFN and format it correctly. The resolved value is passed to the OS `AddUserAsync2` overload and carried inside the returned `SandboxId` so later phases recover the identity actually used without the caller re-supplying it. Validated structurally only (no control characters, at most 256 characters); rejections surface as `MxcError` with `code: 'policy_validation'`. Beyond PFN substitution, whitespace and case are preserved exactly, and on an unpackaged host an explicitly supplied empty string is a **distinct** value from omitting the field. Provision-phase only β€” it is fixed for the sandbox's lifetime, and the `IsolationSessionStartConfig` type rejects it at compile time. |

**Metadata (`IsolationSessionProvisionMetadata`):**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
[tool]
windows_bindgen_version = "0.62.1"
target_windows_crate = "0.62"
generated_date = "2026-07-01"
generated_date = "2026-08-10"
2 changes: 1 addition & 1 deletion schemas/dev/mxc-config.schema.0.8.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@
"description": "Provision-phase IsolationSession configuration (state-aware lifecycle).\n\nThe only phase that takes a per-phase payload, so it is its own type rather than a shared one: a shared type would advertise its fields on every phase in the generated schema. The domain configs and the SDK types are already split per phase; this keeps the wire model aligned with them.",
"properties": {
"appId": {
"description": "Optional application identifier for the calling application. For a packaged application this is the Package Family Name; for an unpackaged one it may be any string. Carried inside the `sandboxId` so later lifecycle phases can recover it without the caller re-supplying it.",
"description": "Optional identifier for the calling application.\n\n**A packaged application must supply its Package Family Name in the form `PFN:<packageFamilyName>`** (for example `PFN:Contoso.App_8wekyb3d8bbwe`) β€” the literal prefix `PFN:` followed by the PFN. A non-empty value is used verbatim, so a packaged caller that passes a bare PFN without the `PFN:` prefix will **not** get PFN-scoped registration. An unpackaged application may pass any string.\n\nAlternatively, a packaged caller may pass an empty string or omit the field to opt into auto-detection: MXC reads the calling process's Package Family Name and substitutes `PFN:<pfn>` for it. On an unpackaged caller the empty/absent value is kept unchanged.\n\nResolution happens at provision, before the OS call; the resolved value is passed to `IsoSessionOps::AddUserAsync2` to associate the provisioned agent user with its owning app, and is carried inside the `sandboxId` so later lifecycle phases recover the identity actually used without the caller re-supplying it.",
"type": [
"string",
"null"
Expand Down
Loading
Loading