From 033ea21135d15d8a91d7541e88070eb4a16d552c Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 30 Jul 2026 15:17:07 -0700 Subject: [PATCH 1/5] Add backend support probe API design & discussion doc Design-only doc for a read-only Rust available_backends() host-capability probe: API shape, isolation-tier ceiling model, current per-backend detection methods and their risks, the remaining probe gap, testing, and follow-up work. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/backend-support-probe-api-plan.md | 176 +++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 docs/backend-support-probe-api-plan.md diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md new file mode 100644 index 000000000..0999ead50 --- /dev/null +++ b/docs/backend-support-probe-api-plan.md @@ -0,0 +1,176 @@ + + +# Backend Support Probe API - Design & Discussion + +> **Status:** Design Proposal + +## 1. Purpose + +Provide a read-only Rust API that reports **which containment backends the +current host can actually run**. Callers can read at startup +to choose a backend without attempting an execution. For the Windows +process-container backend it also reports the **effective isolation tier** the +host supports. + + + +```rust +// One host-available backend, plus its effective isolation tier (if any). +pub struct AvailableBackend { + /// Canonical wire name, e.g. "processcontainer", "seatbelt". + pub backend: String, + /// The highest-isolation tier the host supports for this backend, if the + /// backend has a tier ladder. `None` for backends with no tiers. The string + /// values are the canonical `IsolationTier::as_str()` names (not free-form), + /// and the field is omitted from JSON when `None`. + #[serde(skip_serializing_if = "Option::is_none")] + pub tier: Option, +} + +/// Probe the host and return only the backends it can currently run. +/// An empty `Vec` means "no backend this API can currently affirm on this host" +/// (e.g. an unsupported platform, Linux without `bwrap`, or macOS without +/// `sandbox-exec`) — it is a normal result, not an error. +pub fn available_backends() -> Vec; +``` + +Example results: +| Host | Result | +| --- | --- | +| Stock Windows | `[{ backend: "processcontainer", tier: Some("appcontainer-dacl") }]` | +| Windows w/ BaseContainer | `[{ backend: "processcontainer", tier: Some("base-container") }]` | +| macOS | `[{ backend: "seatbelt", tier: None }]` | +| Linux w/ bwrap + lxc | `[{ backend: "bubblewrap", tier: None }, { backend: "lxc", tier: None }]` | +## 2. Guiding principle +This API answers **"what can I use here?"**, not **"what is the full capability +matrix of this machine?"**. +- We return **only** host-available backends. Nothing is reported as `false`. +- A backend's **absence** means "not currently usable, **for any reason**" +- For per-backend **diagnostics and reasons**, the tool is `wxc-exec --probe` +## 3. Detection & isolation tiers + +### 3.1 Detection methods used today (and their risks) + +Four backends already have *some* presence check, but they are spread across two +layers and vary in how much they actually prove. Documenting them here so the +probe API can reuse the Rust ones and knowingly accept the risk of the shallower +ones. + +| Backend | How presence is detected today | Where | Risk with this method | +| --- | --- | --- | --- | +| `base-container` (process-container tier) | `fallback_detector::is_base_container_usable()` loads `processmodel.dll` and calls an OS capability/create API — no process or VM launch; result cached in a `OnceLock`. | Rust | A cached result (`true` **or** `false`) can go stale if BaseContainer enablement changes mid-process, and the probe is not perfectly pure since it loads a DLL. | +| `windows_sandbox` | `isWindowsSandboxAvailable()` runs `dism /online /get-featureinfo /featurename:Containers-DisposableClientVM` and looks for `State : Enabled`; if DISM throws (usually non-elevated) it falls back to `fs.existsSync(%SystemRoot%\System32\WindowsSandbox.exe)`; result cached. | TypeScript SDK | `dism /online` needs elevation, so a non-elevated caller can't tell *disabled* from *no permission* and drops to the exe-existence check — which proves the feature is installed, not that a sandbox VM can boot. Rust callers get nothing. | +| `lxc` | `isLxcAvailable()` runs `lxc-ls --version`; a clean exit means available. | TypeScript SDK | Only proves the `lxc-ls` CLI is on `PATH` — not that liblxc is loadable or that the caller has the namespaces/cgroup/privileges to actually start a container, so it can report available on a host where a real run fails. Rust callers get nothing. | +| `wslc` | `WslcSdk::load()` loads `wslcsdk.dll` from the executable's own directory (anti-hijack) and validates that every required export resolves. | Rust (execute path) | Runs on the *execute* path, not as a cheap standalone probe: it actually loads the DLL and resolves symbols. Proves the SDK runtime loads, not that a WSL distro/runtime is functional. Feature-gated. | + +### 3.2 Isolation tiers (process-container only) + +Only the Windows process-container backend has a within-backend tier ladder. The +three tiers, and the **policy-free** checks that decide whether each is +reachable, already exist in `appcontainer_common`: + +| Tier | Reachable when | Detector | +| --- | --- | --- | +| `base-container` | BaseContainer API is **usable** (not merely symbol-present) | `fallback_detector::is_base_container_usable()` (the cached wrapper) | +| `appcontainer-bfs` | built with the `tier2_bfs` feature | `cfg!(feature = "tier2_bfs")` | +| `appcontainer-dacl` | always (universal Windows floor) | — | + + + +## 4. The probe gap today +Four backends have no *probe-suitable* (cheap, no persistent host mutation, no +process/VM launch) host detector in Rust today, so a truthful availability signal +for them **cannot be built just yet**. (`lxc` and `wslc` are handled separately — +their checks are documented in §3.1 and accepted as good enough.) + +| Backend | What a real probe needs | What exists today | Risk if faked | +| --- | --- | --- | --- | +| `windows_sandbox` | DISM/registry check of the *Containers-DisposableClientVM* optional feature | only a private "is the `.exe` on disk" check | reports available when the feature is off → launch fails | +| `isolation_session` | build ≥ 26300.8553 **and** `IsoSessionApp.dll` resolvable **and** feature compiled | build gate lives **only in the TypeScript SDK** | wrong OS builds falsely pass | +| `microvm` | hypervisor (WHP) present | nothing | a naive check could **boot a VM** just to test | +| `hyperlight` | hypervisor present + feature compiled | nothing | same VM-boot risk | + + + + +## 5. Testing +- Every returned `backend` is a valid `wxc_common::wire::Containment` name. +- On Windows the result contains `processcontainer` with a `tier` of one of the three known strings; +`appcontainer-dacl` is the floor when nothing higher is reachable. +- On non-Windows, `processcontainer` never appears. +- On macOS, `seatbelt` appears with `tier: None` when `/usr/bin/sandbox-exec `exists. +- A serde snapshot pinning the camelCase JSON shape +(`{"backend":"…","tier":"…"}`), with `tier` **omitted** when `None` +(`#[serde(skip_serializing_if = "Option::is_none")]`) never serialized as `null`. +- Every non-`None` `tier` is one of the canonical `IsolationTier::as_str() `strings, +guarding against drift between this API and the tier ladder. +- On Linux, `bubblewrap` and `lxc` each appear when their check passes (`bwrap --version` / `lxc-ls --version`). +- `wslc` appears when `WslcSdk::load()` resolves `wslcsdk.dll`; the remaining VM group +(`windows_sandbox`, `isolation_session`, `microvm`, `hyperlight`) never appears until its +detector lands. + +## 6. Follow-up work items + +Writing the missing detectors, one issue per backend: +1. `windows_sandbox` - optional-feature (DISM/registry) detector. +2. `isolation_session` - port the build-number + `IsoSessionApp.dll` gate from TypeScript to Rust. +3. `microvm` / `hyperlight` - hypervisor-presence probe. +--- + +## 7. Appendix - Decisions & Notes + +### 7.1 Separate from `platform_support()` + +`platform_support()` (in `mxc_engine::platform`) answers a deliberately +narrower question: "Which backends can the `mxc-sdk` library actually launch?" +Its `available_methods` list is contractually the subset the SDK can drive. On +Linux it may only ever report `bubblewrap`, and unit tests lock that down. +`available_backends()` answers the broader host-capability question, so it is +a separate function. + +### 7.2 Single `tier: Option` + +The fallback detector selects exactly **one** tier, so a per-tier availability +vector is overkill for a menu. + +### 7.3 Effective tier is a ceiling, not a guarantee + +The named tier is the strongest isolation the host is capable +of. A real request can still end up **lower**: some policy options force a +weaker tier (e.g. `deniedPaths` on a host without `SANDBOX_CAP_FS_DENY` +support, or `preferBaseContainer=false`). + +### 7.4 Which tier gets named is based on precedence, not policy + +A host can support several tiers at once. Rather than run your request to see +which one it would pick, the API just names the **strongest** tier the host can +do, by a fixed ranking. Because it never takes a request, it performs none of the +policy-dependent host permission checks that `fallback_detector::detect()` does +(and none of the later `DaclManager` ACE writes that real dispatch performs). It +is purely a reachability walk over the tier ladder. + +### 7.5 The `base-container` tier uses `is_base_container_usable()` + +This loads `processmodel.dll` and calls an OS capability/create API but it +never launches a process or VM to check. + +### 7.6 `process` and `vm` are deliberately excluded + +They are *abstract intents*, not backends with their own runner. + +### 7.7 `base-container` detection caching + +`fallback_detector::is_base_container_usable()` caches its result in a +`OnceLock`. So "fresh detection on every call" is not fully achievable for the +`base-container` tier, and a cached `true` can go **stale** if BaseContainer +enablement changes mid-process. Both are accepted and documented rather than +worked around. + +### 7.8 Ordering + +Results are returned in a stable order, but callers +should **match by `backend` name, not by position**, so the order is free to +change without breaking anyone. \ No newline at end of file From 32e6b2d50dcba46857ea188bab6ebe7a521e5d69 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Fri, 31 Jul 2026 11:10:49 -0700 Subject: [PATCH 2/5] Added Rust vs TS --- docs/backend-support-probe-api-plan.md | 77 ++++++++++++++++++++------ 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md index 0999ead50..d9aeeff95 100644 --- a/docs/backend-support-probe-api-plan.md +++ b/docs/backend-support-probe-api-plan.md @@ -9,11 +9,9 @@ Licensed under the MIT License. ## 1. Purpose -Provide a read-only Rust API that reports **which containment backends the -current host can actually run**. Callers can read at startup -to choose a backend without attempting an execution. For the Windows -process-container backend it also reports the **effective isolation tier** the -host supports. +Provide a read-only Rust API that reports which containment backends the +current host can actually run. Callers can read at startup +to choose a backend without attempting an execution. @@ -45,11 +43,12 @@ Example results: | macOS | `[{ backend: "seatbelt", tier: None }]` | | Linux w/ bwrap + lxc | `[{ backend: "bubblewrap", tier: None }, { backend: "lxc", tier: None }]` | ## 2. Guiding principle -This API answers **"what can I use here?"**, not **"what is the full capability -matrix of this machine?"**. +This API answers **"what can I use here?"**, not "what is the full capability +matrix of this machine?". - We return **only** host-available backends. Nothing is reported as `false`. - A backend's **absence** means "not currently usable, **for any reason**" - For per-backend **diagnostics and reasons**, the tool is `wxc-exec --probe` +- Each capability is **detected once, in Rust**, and the TypeScript SDK projects that result rather than re-checking. ## 3. Detection & isolation tiers ### 3.1 Detection methods used today (and their risks) @@ -61,10 +60,10 @@ ones. | Backend | How presence is detected today | Where | Risk with this method | | --- | --- | --- | --- | -| `base-container` (process-container tier) | `fallback_detector::is_base_container_usable()` loads `processmodel.dll` and calls an OS capability/create API — no process or VM launch; result cached in a `OnceLock`. | Rust | A cached result (`true` **or** `false`) can go stale if BaseContainer enablement changes mid-process, and the probe is not perfectly pure since it loads a DLL. | -| `windows_sandbox` | `isWindowsSandboxAvailable()` runs `dism /online /get-featureinfo /featurename:Containers-DisposableClientVM` and looks for `State : Enabled`; if DISM throws (usually non-elevated) it falls back to `fs.existsSync(%SystemRoot%\System32\WindowsSandbox.exe)`; result cached. | TypeScript SDK | `dism /online` needs elevation, so a non-elevated caller can't tell *disabled* from *no permission* and drops to the exe-existence check — which proves the feature is installed, not that a sandbox VM can boot. Rust callers get nothing. | -| `lxc` | `isLxcAvailable()` runs `lxc-ls --version`; a clean exit means available. | TypeScript SDK | Only proves the `lxc-ls` CLI is on `PATH` — not that liblxc is loadable or that the caller has the namespaces/cgroup/privileges to actually start a container, so it can report available on a host where a real run fails. Rust callers get nothing. | -| `wslc` | `WslcSdk::load()` loads `wslcsdk.dll` from the executable's own directory (anti-hijack) and validates that every required export resolves. | Rust (execute path) | Runs on the *execute* path, not as a cheap standalone probe: it actually loads the DLL and resolves symbols. Proves the SDK runtime loads, not that a WSL distro/runtime is functional. Feature-gated. | +| `base-container` | `fallback_detector::is_base_container_usable()` loads `processmodel.dll`
and calls an OS capability/create API.
No process or VM launch; result cached in a `OnceLock`. | Rust | A cached result (`true` **or** `false`) can go stale if BaseContainer enablement
changes mid-process, and the probe is not perfectly pure since it loads a DLL. | +| `windows_sandbox` | `isWindowsSandboxAvailable()` runs `dism /online /get-featureinfo `
`/featurename:Containers-DisposableClientVM ` and looks for `State : Enabled`
if DISM throws (usually non-elevated) it falls back to
`fs.existsSync(%SystemRoot%\\System32\\WindowsSandbox.exe)`; result cached. | TypeScript SDK | `dism /online` needs elevation, so a non-elevated caller can't tell *disabled* from
*no permission* and drops to the exe-existence checkwhich proves the feature is installed,
not that a sandbox VM can boot. (Will move to Rust) | +| `lxc` | `isLxcAvailable()` runs `lxc-ls --version`; a clean exit means available. | TypeScript SDK | Only proves the `lxc-ls` CLI is on `PATH`, not that liblxc is loadable or that the caller has
the privileges to actually start a container, so it can report available on a host where a real run fails.
(Will move to Rust) | +| `wslc` | `WslcSdk::load()` loads `wslcsdk.dll` from the executable's own directory;
validates that every required export resolves. | Rust (execute path) | Runs on the *execute* path, not as a cheap standalone probe:
it actually loads the DLL and resolves symbols. Proves the SDK runtime loads,
not that a WSL distro/runtime is functional. Feature-gated. | ### 3.2 Isolation tiers (process-container only) @@ -81,10 +80,12 @@ reachable, already exist in `appcontainer_common`: ## 4. The probe gap today + +### 4.1 Backends still missing a Rust detector + Four backends have no *probe-suitable* (cheap, no persistent host mutation, no process/VM launch) host detector in Rust today, so a truthful availability signal -for them **cannot be built just yet**. (`lxc` and `wslc` are handled separately — -their checks are documented in §3.1 and accepted as good enough.) +for them **cannot be built just yet**. | Backend | What a real probe needs | What exists today | Risk if faked | | --- | --- | --- | --- | @@ -93,6 +94,21 @@ their checks are documented in §3.1 and accepted as good enough.) | `microvm` | hypervisor (WHP) present | nothing | a naive check could **boot a VM** just to test | | `hyperlight` | hypervisor present + feature compiled | nothing | same VM-boot risk | +### 4.2 The parity rule: detect once, project into TS + +Detection is split by layer today : `base-container`/`wslc` are Rust-only, while +`windows_sandbox`/`lxc` are TypeScript-only, so the two layers can and already +do disagree. The fix is: detect each capability in exactly one place (Rust), and +have TypeScript read that result rather than compute its own. + +| Step | What | Why it gives parity | +| --- | --- | --- | +| 1. Consolidate detectors in Rust | Port the two TypeScript-only checks (`windows_sandbox` DISM/feature check, `lxc-ls`) into Rust
so `available_backends()` covers every backend. | Each backend has exactly one detector. | +| 2. TypeScript stops probing itself | `getPlatformSupport()` reads the native probe instead of running its own `dism`/`lxc-ls`.
It already does this for the Windows tier via `populateIsolationFromProbe()` → `wxc-exec --probe`;
extend that JSON to carry the backend list. | TypeScript becomes a pure projection of the Rust result. | +| 3. Names flow from serde | Backend names (`Containment`) and tier strings (`IsolationTier::as_str()`) are already Rust-serialized;
TypeScript consumes them as-is instead of hand-re-encoding. | Removes the wire-name drift class structurally. | + +See §7.9 for why the canonical probe stays in Rust rather than moving into the TypeScript layer. + @@ -109,15 +125,20 @@ their checks are documented in §3.1 and accepted as good enough.) guarding against drift between this API and the tier ladder. - On Linux, `bubblewrap` and `lxc` each appear when their check passes (`bwrap --version` / `lxc-ls --version`). - `wslc` appears when `WslcSdk::load()` resolves `wslcsdk.dll`; the remaining VM group -(`windows_sandbox`, `isolation_session`, `microvm`, `hyperlight`) never appears until its -detector lands. +(`windows_sandbox`, `isolation_session`, `microvm`, `hyperlight`) never appears until its detector lands. +- The TypeScript `getPlatformSupport()` output matches the native probe (parity by projection, §4.2), + guarding against the two layers drifting. ## 6. Follow-up work items -Writing the missing detectors, one issue per backend: +Writing the missing detectors and wiring the TypeScript projection, one issue each: 1. `windows_sandbox` - optional-feature (DISM/registry) detector. 2. `isolation_session` - port the build-number + `IsoSessionApp.dll` gate from TypeScript to Rust. 3. `microvm` / `hyperlight` - hypervisor-presence probe. +4. `lxc` - port the `lxc-ls` presence check from TypeScript to Rust, +so the probe (not just the SDK) can report it (§4.2, step 1). +5. TypeScript projection - make `getPlatformSupport()` read the native probe (`wxc-exec --probe` / `mxc_ffi`) +instead of running its own `dism`/`lxc-ls`, so the two layers can't drift (§4.2, step 2). --- ## 7. Appendix - Decisions & Notes @@ -173,4 +194,26 @@ worked around. Results are returned in a stable order, but callers should **match by `backend` name, not by position**, so the order is free to -change without breaking anyone. \ No newline at end of file +change without breaking anyone. + +### 7.9 Why Rust, not the TS layer + +The decision to keep the probe in Rust comes down to one asymmetry: +the *easy* checks are equally easy in Rust, while the *hard* Windows check +is Rust-only either way. + +| Aspect | TS layer | Rust core | +| --- | --- | --- | +| Cheap CLI/feature checks (`lxc-ls`, `bwrap`, `dism`, build number) | Already present; ergonomic `execSync` | Equally cheap as `platform_support()` already
shells `bwrap --version` | +| Windows isolation **tier** | Cannot compute it:
`populateIsolationFromProbe()` shells out to `wxc-exec --probe`
and parses its JSON `tier` | Native — `is_base_container_usable()` loads
`processmodel.dll` and calls the OS API directly | +| Non-Node consumers (`mxc-sdk`, `mxc_ffi` → C# SDK, executor/CLI) | Must shell out to Node or duplicate the logic | First-class; call the API directly | +| Source of truth for wire names / tier strings | Hand-re-encoded from Rust → drift | Owns `Containment` and `IsolationTier::as_str()` | +| Existing drift | Widens it since TS reports `[lxc, bubblewrap]`, Rust reports `[bubblewrap]` | A single probe eliminates the disagreement | +| Stated architectural goal | Reverses the `platform.rs` goal of *"stop depending on the*
*TypeScript SDK for platform discovery"* | Advances it | + + + +Decision: keep the canonical probe in Rust (single source of truth, +reused by `mxc-sdk` / `mxc_ffi` / CLI), and let the TS `getPlatformSupport()` +become a thin wrapper over the native probe instead of re-implementing the +checks. \ No newline at end of file From 6f1358bfea8ea8f8aa943a0b8becb7cf05b4a82b Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Fri, 31 Jul 2026 11:18:32 -0700 Subject: [PATCH 3/5] Apply batched suggestions from Copilot's code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/backend-support-probe-api-plan.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md index d9aeeff95..931f94d87 100644 --- a/docs/backend-support-probe-api-plan.md +++ b/docs/backend-support-probe-api-plan.md @@ -17,6 +17,7 @@ to choose a backend without attempting an execution. ```rust // One host-available backend, plus its effective isolation tier (if any). +#[derive(Debug, Clone, serde::Serialize)] pub struct AvailableBackend { /// Canonical wire name, e.g. "processcontainer", "seatbelt". pub backend: String, @@ -30,8 +31,8 @@ pub struct AvailableBackend { /// Probe the host and return only the backends it can currently run. /// An empty `Vec` means "no backend this API can currently affirm on this host" -/// (e.g. an unsupported platform, Linux without `bwrap`, or macOS without -/// `sandbox-exec`) — it is a normal result, not an error. +/// (e.g. an unsupported platform, Linux with neither `bwrap` nor `lxc`, or +/// macOS without `sandbox-exec`) — it is a normal result, not an error. pub fn available_backends() -> Vec; ``` @@ -91,7 +92,7 @@ for them **cannot be built just yet**. | --- | --- | --- | --- | | `windows_sandbox` | DISM/registry check of the *Containers-DisposableClientVM* optional feature | only a private "is the `.exe` on disk" check | reports available when the feature is off → launch fails | | `isolation_session` | build ≥ 26300.8553 **and** `IsoSessionApp.dll` resolvable **and** feature compiled | build gate lives **only in the TypeScript SDK** | wrong OS builds falsely pass | -| `microvm` | hypervisor (WHP) present | nothing | a naive check could **boot a VM** just to test | +| `microvm` | feature compiled, NanVix runtime files staged, and WHP usable on Windows or `/dev/kvm` readable/writable on Linux | nothing | checking only a hypervisor can report availability when required runtime files are missing | | `hyperlight` | hypervisor present + feature compiled | nothing | same VM-boot risk | ### 4.2 The parity rule: detect once, project into TS @@ -117,11 +118,11 @@ See §7.9 for why the canonical probe stays in Rust rather than moving into the - On Windows the result contains `processcontainer` with a `tier` of one of the three known strings; `appcontainer-dacl` is the floor when nothing higher is reachable. - On non-Windows, `processcontainer` never appears. -- On macOS, `seatbelt` appears with `tier: None` when `/usr/bin/sandbox-exec `exists. -- A serde snapshot pinning the camelCase JSON shape -(`{"backend":"…","tier":"…"}`), with `tier` **omitted** when `None` -(`#[serde(skip_serializing_if = "Option::is_none")]`) never serialized as `null`. -- Every non-`None` `tier` is one of the canonical `IsolationTier::as_str() `strings, +- On macOS, `seatbelt` appears with `tier: None` when `/usr/bin/sandbox-exec` exists. +- A serde snapshot pins the camelCase JSON shape +(`{"backend":"…","tier":"…"}`) and verifies that `tier` is **omitted** when `None` +(`#[serde(skip_serializing_if = "Option::is_none")]`), never serialized as `null`. +- Every non-`None` `tier` is one of the canonical `IsolationTier::as_str()` strings, guarding against drift between this API and the tier ladder. - On Linux, `bubblewrap` and `lxc` each appear when their check passes (`bwrap --version` / `lxc-ls --version`). - `wslc` appears when `WslcSdk::load()` resolves `wslcsdk.dll`; the remaining VM group From 97aece48d0d4db1f315a8d0e835d53f6128eae86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:28:24 +0000 Subject: [PATCH 4/5] docs: require side-effect-free transport in backend probe API plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The §4.2 step 2 previously said to "extend that JSON to carry the backend list" via `wxc-exec --probe`. This violates the proposed read-only contract: `wxc/src/main.rs:750-772` runs `recover_orphaned_state()` *before* handling `--probe`, which can restore/prune host DACL state. Update the plan to: - Require a side-effect-free transport (e.g. a new `--available-backends` mode handled before DACL recovery, or `mxc_ffi`) instead of extending `--probe` unchanged. - Explicitly note the constraint in follow-up work item 5. --- docs/backend-support-probe-api-plan.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md index 931f94d87..5593bf22f 100644 --- a/docs/backend-support-probe-api-plan.md +++ b/docs/backend-support-probe-api-plan.md @@ -105,7 +105,7 @@ have TypeScript read that result rather than compute its own. | Step | What | Why it gives parity | | --- | --- | --- | | 1. Consolidate detectors in Rust | Port the two TypeScript-only checks (`windows_sandbox` DISM/feature check, `lxc-ls`) into Rust
so `available_backends()` covers every backend. | Each backend has exactly one detector. | -| 2. TypeScript stops probing itself | `getPlatformSupport()` reads the native probe instead of running its own `dism`/`lxc-ls`.
It already does this for the Windows tier via `populateIsolationFromProbe()` → `wxc-exec --probe`;
extend that JSON to carry the backend list. | TypeScript becomes a pure projection of the Rust result. | +| 2. TypeScript stops probing itself | `getPlatformSupport()` reads the native backend-availability result instead of running its own `dism`/`lxc-ls`.
The transport must be **side-effect-free**: `wxc-exec --probe` runs *after* `recover_orphaned_state()` (which can restore/prune DACL state on the host), so it cannot be extended unchanged without violating the read-only contract.
Instead, expose backend availability via a dedicated mode handled **before** DACL recovery (e.g. `wxc-exec --available-backends`), or directly through `mxc_ffi`. | TypeScript becomes a pure projection of the Rust result, without triggering host mutation. | | 3. Names flow from serde | Backend names (`Containment`) and tier strings (`IsolationTier::as_str()`) are already Rust-serialized;
TypeScript consumes them as-is instead of hand-re-encoding. | Removes the wire-name drift class structurally. | See §7.9 for why the canonical probe stays in Rust rather than moving into the TypeScript layer. @@ -138,8 +138,12 @@ Writing the missing detectors and wiring the TypeScript projection, one issue ea 3. `microvm` / `hyperlight` - hypervisor-presence probe. 4. `lxc` - port the `lxc-ls` presence check from TypeScript to Rust, so the probe (not just the SDK) can report it (§4.2, step 1). -5. TypeScript projection - make `getPlatformSupport()` read the native probe (`wxc-exec --probe` / `mxc_ffi`) -instead of running its own `dism`/`lxc-ls`, so the two layers can't drift (§4.2, step 2). +5. TypeScript projection - make `getPlatformSupport()` read the native backend availability via a +side-effect-free transport (e.g. a new `wxc-exec --available-backends` mode handled **before** +`recover_orphaned_state()`, or `mxc_ffi`) instead of running its own `dism`/`lxc-ls`, so the two +layers can't drift (§4.2, step 2). Do **not** extend the existing `--probe` flag: it runs after +`recover_orphaned_state()`, which can restore/prune DACL state and would violate the read-only +contract of this API. --- ## 7. Appendix - Decisions & Notes From 3fd7c43ab77ec541b602af1997ffe2c04887a0f3 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Fri, 7 Aug 2026 09:37:03 -0700 Subject: [PATCH 5/5] docs(probe): define Stock Windows + update isolation_session detection Address @bbonaby's review notes on #717: - Define what 'Stock Windows' means (clean install, default optional features -> appcontainer-dacl floor) and link docs/process-container/os-version-support.md near the examples table. And reflect @adpa-ms's change in #761: - isolation_session availability is now detected by whether the Windows.AI.IsolationSession.Preview IsoSessionOps API class is registered on the OS (activation-factory resolves), not a build-number gate (26300.8553). Update the probe-gap table row and the follow-up work item accordingly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/backend-support-probe-api-plan.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md index 5593bf22f..bc814572f 100644 --- a/docs/backend-support-probe-api-plan.md +++ b/docs/backend-support-probe-api-plan.md @@ -37,6 +37,12 @@ pub fn available_backends() -> Vec; ``` Example results: +> **"Stock Windows"** here means a clean Windows install with only the default +> optional features enabled (no BaseContainer, Windows Sandbox, etc.), so the +> process-container backend falls to its `appcontainer-dacl` floor. See +> [`docs/process-container/os-version-support.md`](process-container/os-version-support.md) +> for the per-release policy-support matrix that determines the reachable tier. + | Host | Result | | --- | --- | | Stock Windows | `[{ backend: "processcontainer", tier: Some("appcontainer-dacl") }]` | @@ -91,7 +97,7 @@ for them **cannot be built just yet**. | Backend | What a real probe needs | What exists today | Risk if faked | | --- | --- | --- | --- | | `windows_sandbox` | DISM/registry check of the *Containers-DisposableClientVM* optional feature | only a private "is the `.exe` on disk" check | reports available when the feature is off → launch fails | -| `isolation_session` | build ≥ 26300.8553 **and** `IsoSessionApp.dll` resolvable **and** feature compiled | build gate lives **only in the TypeScript SDK** | wrong OS builds falsely pass | +| `isolation_session` | activation of the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` runtime class succeeds (the API class is registered on the OS **and** its OS feature gate is on) **and** the backend feature is compiled | as of #761, detection queries whether the API class is registered rather than gating on a build number; a `CLASS_E_CLASSNOTAVAILABLE` / `REGDB_E_CLASSNOTREG` activation failure means unavailable | none for false-availability now — a machine without the API registered fails activation cleanly; still needs a cheap probe seam so callers don't have to attempt a real activation | | `microvm` | feature compiled, NanVix runtime files staged, and WHP usable on Windows or `/dev/kvm` readable/writable on Linux | nothing | checking only a hypervisor can report availability when required runtime files are missing | | `hyperlight` | hypervisor present + feature compiled | nothing | same VM-boot risk | @@ -134,7 +140,7 @@ guarding against drift between this API and the tier ladder. Writing the missing detectors and wiring the TypeScript projection, one issue each: 1. `windows_sandbox` - optional-feature (DISM/registry) detector. -2. `isolation_session` - port the build-number + `IsoSessionApp.dll` gate from TypeScript to Rust. +2. `isolation_session` - probe whether the `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API class is registered on the OS (activation-factory resolvable), replacing the old build-number gate (see #761), and expose it to Rust. 3. `microvm` / `hyperlight` - hypervisor-presence probe. 4. `lxc` - port the `lxc-ls` presence check from TypeScript to Rust, so the probe (not just the SDK) can report it (§4.2, step 1).