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
11 changes: 11 additions & 0 deletions .github/workflows/Build.Linux.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ jobs:
run_pinned "LXC dispatch arm" -p mxc-sdk --lib streaming_lxc_reaches_the_backend_on_linux
run_pinned "LXC FFI routing" -p mxc_ffi --test ffi extern_spawn_json_reaches_the_lxc_backend
run_pinned "SDK platform helpers" -p mxc-sdk --test sdk_helpers
run_pinned "Probe coverage" -p mxc-sdk --lib mxc_engine::probe
run_pinned "Backend registry" -p mxc-sdk --lib mxc_engine::backend_registry

# Hyperlight is a Linux backend only on x64, and the probe reports it
# from a feature-gated branch the default build never compiles.
if [ -n "${{ matrix.features }}" ]; then
run_pinned "Probe coverage (${{ matrix.features }})" \
--features "${{ matrix.features }}" -p mxc-sdk --lib mxc_engine::probe
run_pinned "Backend registry (${{ matrix.features }})" \
--features "${{ matrix.features }}" -p mxc-sdk --lib mxc_engine::backend_registry
fi

# Runs the Bubblewrap executor characterization tests. lxc-exec was built
# into src/target/<triple>/release above, where find_binary() locates it.
Expand Down
9 changes: 5 additions & 4 deletions docs/development/architecture/repository-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ Shared parsing and normalization live in the `mxc_common` module; backend-specif
validation and enforcement live with each backend.

`mxc_engine/src/backend_registry.rs` owns backend registration metadata,
including experimental classification, keyed by the shared `ContainmentBackend`
enum. Runtime authorization consults that registry. Exact-contract publication,
build-feature availability, and host-capability probing remain separate; the
registry neither dispatches workloads nor adds backend dependencies to
including experimental classification and probe coverage, keyed by the shared
`ContainmentBackend` enum. Runtime authorization consults that registry.
Exact-contract publication, build-feature availability, and host-capability
probing remain separate; the registry declares which backends the probe covers
but neither detects them, dispatches workloads, nor adds backend dependencies to
`mxc_common`.

## Request flow
Expand Down
66 changes: 32 additions & 34 deletions docs/development/plans/backend-support-probe-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ ones.
| Backend | How presence is detected today | Where | Risk with this method |
| --- | --- | --- | --- |
| `base-container` | `BaseContainerRunner::is_base_container_api_present()` calls `IsApiSetImplemented` for the process-security-environment API set. It does not load `processmodel.dll`, create a security environment, or start a process. | Rust | This proves that Windows implements the API-set contract; request-specific support bits are queried separately when the policy requires them. |
| `windows_sandbox` | `isWindowsSandboxAvailable()` runs `dism /online /get-featureinfo `<br>`/featurename:Containers-DisposableClientVM ` and looks for `State : Enabled`<br>if DISM throws (usually non-elevated) it falls back to<br>`fs.existsSync(%SystemRoot%\\System32\\WindowsSandbox.exe)`; result cached. | TypeScript SDK | `dism /online` needs elevation, so a non-elevated caller can't tell *disabled* from<br>*no permission* and drops to the exe-existence checkwhich proves the feature is installed,<br>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<br>the privileges to actually start a container, so it can report available on a host where a real run fails.<br>(Will move to Rust) |
| `wslc` | `WslcSdk::load()` loads `wslcsdk.dll` from the executable's own directory;<br>validates that every required export resolves. | Rust (execute path) | Runs on the *execute* path, not as a cheap standalone probe:<br>it actually loads the DLL and resolves symbols. Proves the SDK runtime loads,<br>not that a WSL distro/runtime is functional. Feature-gated. |
| `windows_sandbox` | `isWindowsSandboxAvailable()` runs `dism /online /get-featureinfo `<br>`/featurename:Containers-DisposableClientVM ` and looks for `State : Enabled`<br>if DISM throws (usually non-elevated) it falls back to<br>`fs.existsSync(%SystemRoot%\\System32\\WindowsSandbox.exe)`; result cached. | TypeScript SDK | `dism /online` needs elevation, so a non-elevated caller can't tell *disabled* from<br>*no permission* and drops to the exe-existence checkwhich proves the feature is installed,<br>not that a sandbox VM can boot. (Dupe'd in Rust: `windows_sandbox_lifecycle::availability`) |
| `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<br>the privileges to actually start a container, so it can report available on a host where a real run fails.<br>(Dupe'd in Rust: `lxc_common::availability`) |
| `wslc` | `wslc_common::is_available()` confirms WSL2 and the WSLC runtime are present. | Rust | Resolving `wslcsdk.dll` alone is not sufficient and is not what this checks;<br>`WslcSdk::load()` still runs on the *execute* path. Feature-gated. |
| `isolation_session` | `IsoSessionOps` activates and reports a `LocalAgentUser` feature level above zero. Queries API-class registration rather than a build number (#761); a `CLASS_E_CLASSNOTAVAILABLE` / `REGDB_E_CLASSNOTREG` activation failure means unavailable. | Rust | |
| `hyperlight` | Has a hypervisor check on each supported host β€” `is_whp_available()` on Windows, `is_kvm_available()` on Linux β€” neither of which boots a VM. | Rust | |

### 3.2 Isolation tiers (process-container only)

Expand All @@ -115,40 +117,28 @@ reachable, already exist in `process_container_common`:
| `appcontainer-dacl` | always (universal Windows floor) | β€” |



## 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**.

| 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` | 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 |
### 4.1. Backends with no probe
- `microvm` needs a probe that checks for: (1) feature compiled, (2) NanVix runtime files staged, and (3) WHP usable on Windows or `/dev/kvm` readable/writable on Linux
- `vm` is an abstract intent that resolves to a
concrete backend per host and is never probed in its own right.

### 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.
Rust detects every backend that has a detector, but `getPlatformSupport()` still
runs its own `dism`/`lxc-ls`, so the two layers can 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<br>so `available_backends()` covers every backend. | Each backend has exactly one detector. |
| 1. Consolidate detectors in Rust β€” **landed** | The `windows_sandbox` and `lxc` checks live in Rust<br>(`windows_sandbox_lifecycle::availability`, `lxc_common::availability`), so `available_backends()` covers every backend with a detector. | Each backend has exactly one detector. |
| 2. TypeScript stops probing itself | `getPlatformSupport()` reads the native backend-availability result instead of running its own `dism`/`lxc-ls`.<br>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.<br>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;<br>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.




## 5. Testing
- Every returned `backend` is a valid
`mxc_common::models::ContainmentBackend::wire_name()` value.
Expand All @@ -160,7 +150,9 @@ See Β§7.9 for why the canonical probe stays in Rust rather than moving into the
(`{"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.
guarding against drift between this API and the tier ladder. The canonical list is derived from
`IsolationTier::ALL`, and a further test asserts every tier is reachable from `select_tier`, so a
new tier cannot land without probe coverage.
- On Linux, `bubblewrap` and `lxc` each appear when their check passes (`bwrap --version` / `lxc-ls --version`).
- On Linux, `bubblewrap` carries the `proxyEnforcement` capability when the private-namespace
dependencies are present, and a `warnings` entry naming the missing dependency when they are not;
Expand All @@ -169,20 +161,26 @@ the backend itself still appears either way. Both directions are covered by exer
test host happens to have `slirp4netns` installed.
- A serde snapshot pins the camelCase capability name (`"proxyEnforcement"`) and verifies that
`capabilities` and `warnings` are **omitted** when empty.
- `wslc` appears when `WslcSdk::load()` resolves `wslcsdk.dll`; the remaining VM group
(`windows_sandbox`, `isolation_session`, `microvm`, `hyperlight`) never appears until its detector lands.
- `wslc` appears when `wslc_common::is_available()` confirms WSL2 and the WSLC runtime are present;
resolving `wslcsdk.dll` alone is not sufficient. Each backend declares a `ProbeCoverage` in
`mxc_engine::backend_registry`, naming either the targets it is detected on or why it has no
detector. The platform reporting functions take their host checks as parameters, so two tests run
the real reporting with every check forced to pass: one asserts every backend declared detected on
the current target is reported, the other that a backend declared `NotDetected` still never is.
Registering a backend as detected without adding a branch to `available_backends()` fails the first.
- 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 and wiring the TypeScript projection, one issue each:
1. `windows_sandbox` - optional-feature (DISM/registry) detector.
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).
5. TypeScript projection - make `getPlatformSupport()` read the native backend availability via a
Remaining detector and TypeScript-projection work, one issue each. The
`windows_sandbox`, `isolation_session`, and `lxc` detectors have landed in Rust
and are registered as `ProbeCoverage::Detected`.

1. `microvm` - hypervisor-presence probe. `hyperlight` has one per host
(`is_whp_available()` / `is_kvm_available()`); `microvm`
is registered as `ProbeCoverage::NotDetected` until its detector lands.
2. 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
Expand Down
15 changes: 11 additions & 4 deletions scripts/check-dotnet-api-parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,20 @@ for (const member of rustCapabilityMembers) {
}
}

const tierMatch = /const CANONICAL_TIERS:[^=]+=\s*\[([^\]]+)\]/s.exec(
rustProbeFull
const rustTierLadder = read(
"src",
"mxc-sdk",
"src",
"backends",
"process_container",
"common",
"fallback_detector.rs"
);
const tierMatch = /isolation_tiers!\s*\{(.*?)\n\}/s.exec(rustTierLadder);
if (!tierMatch) {
errors.push("probe.rs: could not find CANONICAL_TIERS");
errors.push("fallback_detector.rs: could not find the isolation_tiers! ladder");
} else {
const rustTiers = [...tierMatch[1].matchAll(/"([^"]+)"/g)].map(
const rustTiers = [...tierMatch[1].matchAll(/=>\s*"([^"]+)"/g)].map(
(match) => match[1]
);
const managedTiers = [
Expand Down
Loading
Loading