[WSLC] Promote from experimental to the stable config surface - #1064
[WSLC] Promote from experimental to the stable config surface#1064Soham Das (SohamDas2021) wants to merge 8 commits into
Conversation
WSLc accepted four policy fields, carried them into the runner, and then
never read them. A caller asking for a UI lockdown, a firewall enforcement
mode, inbound local-network access, or policy preservation got a container
that silently did not have the posture they asked for. Close each hole with
an explicit policy_validation rejection.
Every rejection aborts before anything is created. All three entry surfaces
validate first: ScriptRunner::run ahead of execute, SandboxBackend::spawn
ahead of start_container, and the state-aware dispatcher ahead of each phase
body -- and connect_daemon() lives inside provision(), so a refused provision
never even spawns the daemon.
ui rejected on every phase, both surfaces
network.allowLocalNetwork rejected at state-aware provision (one-shot
already rejected it)
network.enforcementMode firewall/both rejected; capabilities accepted
lifecycle.preservePolicy rejected on one-shot (state-aware already
rejects the whole lifecycle section at parse)
ui is rejected by presence, not value. UiPolicy::default() is full lockdown,
so an explicitly supplied lockdown ui is indistinguishable by value from an
absent one -- a value-based check would let the single most restrictive
request a caller can write through unenforced. This uses the parse-derived
ContainerPolicy::ui_specified flag, mirroring IsolationSession.
enforcementMode and preservePolicy are rejected by value instead, because
their defaults honestly describe WSLc's behavior: an all-or-nothing container
network with nothing per-host to enforce, and auto-remove teardown. Refusing
those for mere presence would be dishonest.
destroyOnExit stays honored -- it selects WSLC_CONTAINER_FLAG_AUTO_REMOVE --
so only preservePolicy is refused. A blanket lifecycle rejection would have
broken the wslc_destroy_on_exit_{true,false} configs; a test pins both values
still passing.
The two allowLocalNetwork messages differ deliberately. One-shot points
callers at experimental.wslc portMappings, but WslcProvisionPhase has no
portMappings field at all, so repeating that advice on the state-aware
surface would be a lie.
Rejection ordering is filesystem -> ui -> network, documented in the policy.rs
module header and pinned by precedence tests.
No wire, schema, or parser-gating changes: this is a domain-model behavior
change only, so it lands while WSLc is still experimental and the nightly
WSLc suite exercises the new rejections before the surface moves.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c23ddc86-3848-452e-8355-e11d2ffa9b7f
Closes #1049 WSLc is now configured through the top-level `wslc` section instead of `experimental.wslc`, and selecting `"containment": "wslc"` no longer requires `--experimental` / `{ experimental: true }` / `SandboxRequest::set_experimental(true)`. This mirrors the Seatbelt promotion precedent. Wire + schema - Move `wslc` from `wire::Experimental` to the `MxcConfig` root, and from `experimental.rs` to `stable.rs` in the 0.9.0-alpha closed contract. - Keep `experimental.wslc` as a rejection alias so a pre-promotion config fails with an actionable migration message rather than being silently ignored. - Regenerate the two dev schemas and the two TypeScript wire oracles. Parser + dispatch - Generalize the state-aware dispatcher with `SectionRoot` (`Experimental` | `Stable`); `StatefulSandboxBackend::SECTION_ROOT` defaults to `Experimental` and only `WslcStateAwareRunner` overrides it. `ParsedStateAwareRequest` gains `stable_raw` for stable-rooted backends. - Drop the experimental gate for WSLc on both the one-shot and state-aware surfaces. TypeScript SDK - Move `wslc` out of `ContainerConfig.experimental` and off `ExperimentalBackends`; add `BACKEND_SECTION_ROOT`, the TS twin of the Rust `SectionRoot`, so envelopes are built at the right root. - Add a `wslcAvailable` probe fact end to end (Rust probe -> engine -> `wxc-exec --probe` -> SDK). Without it, removing the experimental gate unmasked the fact that `wslc` was never in `availableMethods`, which would have made the backend unreachable through the SDK. Rust SDK, corpus, harnesses and docs - Drop the `set_experimental(true)` requirement from `mxc-sdk`. - Hoist `experimental.wslc` to top-level `wslc` in 31 test configs. - Remove `--experimental` from the WSLc test harnesses. - Update the WSLc docs, schema reference, SDK READMEs, setup script and `.github/copilot-instructions.md`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9f02fa89-c89e-4a5d-b80d-5130252d3654
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
The promotion updated the WSLc-specific guides and SDK READMEs but left
four documents still describing WSLc as experimental. Two of them are
actively wrong rather than merely stale:
- README.md listed `wslc` among the "Experimental backends" that require
`{ experimental: true }` or `--experimental`. This is the repo's front
page, and the claim is now false.
- docs/linux-wsl-roadmap-june-2026.md carried a copy-pasteable JSON
example nesting `wslc` under `experimental` -- the exact shape the
parser now rejects with the migration error. Its "Notes" section also
still listed WSLC alongside Bubblewrap as awaiting promotion.
The remaining two were internally contradictory after the promotion:
- docs/wsl/wsl-container-support-plan.md had its preamble rewritten to
say WSLc is stable while three JSON examples, the dispatch snippet,
the architecture diagram, and a runnable CLI command all still showed
the experimental gate. (The mixed top-level/nested examples predate
this branch; this commit makes the whole document consistent.)
- sdk/dotnet/README.md described `WslcContainment` as selecting "the
experimental WSLC backend" and stated `Experimental` is required. The
sample also set `Experimental = true`, which is now dead weight --
`resolve_runner` gates only MicroVm, WindowsSandbox, IsolationSession,
and Hyperlight, and `require_experimental_optin` gates only
WindowsSandbox and IsolationSession.
Docs only; no behavior change. All 14 edited JSON blocks re-verified as
parseable.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9f02fa89-c89e-4a5d-b80d-5130252d3654
…ing' into sohamdas2021-wslc-promotion-plan
There was a problem hiding this comment.
Pull request overview
Promotes WSLc to the top-level stable configuration surface and removes its experimental opt-in. The current 173-file diff also contains substantial unrelated C#, Bubblewrap, and LXC changes.
Changes:
- Moves WSLc configuration, dispatch, schemas, SDKs, tests, and documentation to the stable surface.
- Adds WSLc availability probing and native sidecar packaging.
- Includes unrelated runtime and SDK changes outside #1049’s stated scope.
Reviewed changes
Copilot reviewed 105 out of 107 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
.github/copilot-instructions.md |
Updates WSLc architecture guidance. |
.github/workflows/Build.Linux.Job.yml |
Adds Bubblewrap SDK tests. |
.github/workflows/SDK.Dotnet.Test.Job.yml |
Adds optional-backend lanes. |
.github/workflows/Versioning.Checks.Job.yml |
Adds C# API parity check. |
README.md |
Lists WSLc as stable. |
build.bat |
Stages WSLc runtime sidecars. |
docs/linux-wsl-roadmap-june-2026.md |
Updates WSLc promotion status. |
docs/schema.md |
Documents top-level WSLc. |
docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md |
Updates lifecycle wire documentation. |
docs/wsl/wsl-container-getting-started.md |
Updates WSLc usage. |
docs/wsl/wsl-container-support-plan.md |
Updates WSLc design status. |
docs/wsl/wslc-state-aware.md |
Removes experimental requirement. |
schemas/dev/mxc-config.schema.0.9.0-alpha.json |
Regenerates exact schema. |
schemas/dev/mxc-config.schema.0.9.0-dev.json |
Regenerates rolling schema. |
schemas/schema-version.json |
Adds WSLc lifecycle version. |
scripts/check-dotnet-api-parity.js |
Adds C# parity validation. |
scripts/check-dotnet-bindings-codegen.js |
Expands binding checks. |
scripts/setup-wslc.ps1 |
Updates configuration paths. |
scripts/versioning/check-schema-versions.js |
Checks WSLc/C# versions. |
sdk/dotnet/CHANGELOG.md |
Documents broad C# changes. |
sdk/dotnet/Microsoft.Mxc.Sdk.Sample/Program.cs |
Uses explicit request containment. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/JsonAssert.cs |
Adds JSON golden comparison. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/Microsoft.Mxc.Sdk.Tests.csproj |
Embeds shared fixtures. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcLifecycleE2ETests.cs |
Expands lifecycle coverage. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcLifecycleTests.cs |
Tests lifecycle envelopes/features. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxProcessTests.cs |
Expands streaming tests. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxTests.cs |
Tests requests and discovery. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/SandboxAdapterTests.cs |
Tests SDK adapters. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/SandboxPolicyDiscoveryTests.cs |
Tests policy discovery. |
sdk/dotnet/Microsoft.Mxc.Sdk/Microsoft.Mxc.Sdk.csproj |
Builds/packages optional backends. |
sdk/dotnet/Microsoft.Mxc.Sdk/MxcLifecycle.cs |
Adds multi-backend lifecycle support. |
sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandbox.cs |
Expands managed SDK surface. |
sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandboxProcess.cs |
Changes streaming lifecycle behavior. |
sdk/dotnet/Microsoft.Mxc.Sdk/Native/SafeHandles.cs |
Adds stream-closer handle. |
sdk/dotnet/Microsoft.Mxc.Sdk/NetworkProxyPolicyJsonConverter.cs |
Adds proxy-union conversion. |
sdk/dotnet/Microsoft.Mxc.Sdk/PlatformDiscovery.cs |
Adds typed platform discovery. |
sdk/dotnet/Microsoft.Mxc.Sdk/SandboxAdapters.cs |
Adds injectable adapters. |
sdk/dotnet/Microsoft.Mxc.Sdk/SandboxId.cs |
Documents WSLc identifiers. |
sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs |
Expands managed policy model. |
sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicyDiscovery.cs |
Adds policy discovery helpers. |
sdk/dotnet/Microsoft.Mxc.Sdk/SandboxRequest.cs |
Adds explicit containment requests. |
sdk/dotnet/Microsoft.Mxc.Sdk/SchemaVersions.cs |
Exposes schema constants. |
sdk/dotnet/Microsoft.Mxc.Sdk/StateAwareTypes.cs |
Adds backend-specific lifecycle types. |
sdk/dotnet/README.md |
Documents expanded C# API. |
sdk/node/README.md |
Updates WSLc SDK documentation. |
sdk/node/src/generated/v0_9_0_alpha/wire.ts |
Regenerates exact wire types. |
sdk/node/src/generated/wire.ts |
Regenerates rolling wire types. |
sdk/node/src/platform.ts |
Reports WSLc availability. |
sdk/node/src/sandbox.ts |
Emits top-level WSLc config. |
sdk/node/src/state-aware-helper.ts |
Supports stable section roots. |
sdk/node/src/state-aware-types.ts |
Updates WSLc paths. |
sdk/node/src/types.ts |
Removes WSLc from experimental types. |
sdk/node/tests/integration/wslc-e2e.test.ts |
Updates WSLc E2E configuration. |
sdk/node/tests/unit/conformance-helpers.ts |
Updates conformance documentation. |
sdk/node/tests/unit/sandbox.test.ts |
Tests stable WSLc behavior. |
sdk/node/tests/unit/state-aware.test.ts |
Tests stable lifecycle envelopes. |
sdk/node/tests/unit/wire-conformance-state-aware.test.ts |
Updates lifecycle conformance. |
src/Cargo.lock |
Records dependency changes. |
src/backends/appcontainer/common/src/probe.rs |
Adds WSLc probe fact. |
src/backends/bubblewrap/common/src/bwrap_command.rs |
Adds process-tree teardown flag. |
src/backends/bubblewrap/common/src/bwrap_runner.rs |
Updates Bubblewrap teardown handling. |
src/backends/bubblewrap/common/src/bwrap_version.rs |
Documents required flag version. |
src/backends/lxc/common/src/lxc_runner.rs |
Adds network-readiness enforcement. |
src/backends/wslc/common/src/daemon_client.rs |
Resolves daemon beside native module. |
src/backends/wslc/common/src/daemon_protocol.rs |
Updates public-path documentation. |
src/backends/wslc/common/src/policy.rs |
Updates WSLc policy paths. |
src/backends/wslc/common/src/state_aware.rs |
Selects stable section root. |
src/backends/wslc/common/src/wsl_container_runner.rs |
Updates stable WSLc paths. |
src/backends/wslc/common/src/wslc_bindings.rs |
Resolves SDK DLL beside module. |
src/core/mxc-sdk/Cargo.toml |
Updates feature/test dependencies. |
src/core/mxc-sdk/README.md |
Removes WSLc opt-in requirement. |
src/core/mxc-sdk/src/lib.rs |
Updates WSLc API documentation. |
src/core/mxc-sdk/tests/streaming_bubblewrap.rs |
Adds Bubblewrap streaming coverage. |
src/core/mxc_config_contract/src/dev/experimental.rs |
Removes experimental WSLc contract. |
src/core/mxc_config_contract/src/dev/mod.rs |
Re-exports stable WSLc types. |
src/core/mxc_config_contract/src/dev/one_shot.rs |
Adds top-level WSLc field. |
src/core/mxc_config_contract/src/dev/stable.rs |
Defines stable WSLc contract. |
src/core/mxc_config_contract/src/dev/state_aware/provision/wslc.rs |
Moves provision configuration. |
src/core/mxc_config_contract/tests/v0_9_0_alpha.rs |
Registers WSLc tests. |
src/core/mxc_config_contract/tests/v0_9_0_alpha/experimental.rs |
Removes old test module. |
src/core/mxc_config_contract/tests/v0_9_0_alpha/experimental/root.rs |
Tests alias rejection. |
src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/invalid/port_out_of_range.json |
Updates invalid WSLc fixture. |
src/core/mxc_config_contract/tests/v0_9_0_alpha/optional_fields.rs |
Updates WSLc optional paths. |
src/core/mxc_config_contract/tests/v0_9_0_alpha/state_aware/provision/wslc.rs |
Tests WSLc provision contract. |
src/core/mxc_config_contract/tests/v0_9_0_alpha/wslc.rs |
Tests stable WSLc contract. |
src/core/mxc_config_contract/tests/version_boundaries.rs |
Registers boundary tests. |
src/core/mxc_config_contract/tests/version_boundaries/experimental.rs |
Removes experimental boundary case. |
src/core/mxc_config_contract/tests/version_boundaries/state_aware.rs |
Updates lifecycle boundary fixture. |
src/core/mxc_config_contract/tests/version_boundaries/wslc.rs |
Adds WSLc version boundaries. |
src/core/mxc_engine/src/dispatch.rs |
Removes streaming opt-in gate. |
src/core/mxc_engine/src/guarded_capture.rs |
Reuses module-path resolver. |
src/core/mxc_engine/src/lib.rs |
Exports WSLc availability. |
src/core/mxc_engine/src/platform.rs |
Adds serializable WSLc support. |
src/core/mxc_engine/src/policy.rs |
Builds top-level WSLc config. |
src/core/mxc_engine/src/run.rs |
Removes one-shot opt-in gate. |
src/core/mxc_engine/src/state_aware.rs |
Removes lifecycle opt-in gate. |
src/core/wxc/src/main.rs |
Publishes WSLc probe fact. |
src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs |
Adapts stable WSLc config. |
src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs |
Updates adapter tests. |
src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs |
Adapts stable lifecycle config. |
src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/provision.rs |
Updates provision adapter tests. |
src/core/wxc_common/src/config_contract_adapters/v0_6.rs |
Initializes new wire field. |
src/core/wxc_common/src/config_contract_adapters/v0_7.rs |
Initializes new wire field. |
src/core/wxc_common/src/config_contract_adapters/v0_8.rs |
Initializes new wire field. |
src/core/wxc_common/src/config_parser.rs |
Parses/rejects promoted WSLc shapes. |
src/core/wxc_common/src/models.rs |
Moves WSLc into request model. |
src/core/wxc_common/src/process_util.rs |
Adds native module-path resolver. |
src/core/wxc_common/src/state_aware_backend.rs |
Adds backend section-root metadata. |
src/core/wxc_common/src/state_aware_dispatch.rs |
Dispatches by section root. |
src/core/wxc_common/src/state_aware_request.rs |
Retains stable-rooted raw config. |
src/core/wxc_common/src/wire.rs |
Promotes WSLc wire section. |
src/ffi/mxc_ffi/Cargo.toml |
Adds WSLc feature/dependency. |
src/ffi/mxc_ffi/src/lib.rs |
Expands FFI SDK surface. |
src/ffi/mxc_ffi/src/request.rs |
Adds request-contract handling. |
src/ffi/mxc_ffi/src/state_aware.rs |
Tests shared lifecycle fixtures. |
src/ffi/mxc_ffi/src/streaming.rs |
Expands streaming FFI. |
src/ffi/mxc_ffi/tests/ffi.rs |
Expands FFI integration coverage. |
src/testing/wxc_e2e_tests/tests/e2e_bubblewrap_characterization.rs |
Strengthens timeout teardown test. |
tests/configs/bubblewrap_network_egress_budget_rejected.json |
Adds network-budget fixture. |
tests/configs/bubblewrap_readonly_denial.json |
Adds read-only denial fixture. |
tests/configs/bubblewrap_teardown_timeout.json |
Adds teardown fixture. |
tests/configs/bubblewrap_teardown_timeout_netns.json |
Adds network teardown fixture. |
tests/configs/bubblewrap_version_gate.json |
Adds version-gate fixture. |
tests/configs/wslc_custom_registry.json |
Hoists WSLc section. |
tests/configs/wslc_custom_registry_ghcr.json |
Hoists WSLc section. |
tests/configs/wslc_custom_registry_quay.json |
Hoists WSLc section. |
tests/configs/wslc_denied_dotdot_alias.json |
Hoists WSLc section. |
tests/configs/wslc_denied_masking.json |
Hoists WSLc section. |
tests/configs/wslc_destroy_on_exit_false_rejected.json |
Hoists WSLc section. |
tests/configs/wslc_destroy_on_exit_true.json |
Hoists WSLc section. |
tests/configs/wslc_env_vars.json |
Hoists WSLc section. |
tests/configs/wslc_exit_code.json |
Hoists WSLc section. |
tests/configs/wslc_filesystem.json |
Hoists WSLc section. |
tests/configs/wslc_filesystem_object.json |
Hoists WSLc section. |
tests/configs/wslc_large_output.json |
Hoists WSLc section. |
tests/configs/wslc_most_specific_denied_parent.json |
Hoists WSLc section. |
tests/configs/wslc_network_isolated.json |
Hoists WSLc section. |
tests/configs/wslc_network_proxy.json |
Hoists WSLc section. |
tests/configs/wslc_port_mapping_multiple.json |
Hoists WSLc section. |
tests/configs/wslc_port_mapping_tcp.json |
Hoists WSLc section. |
tests/configs/wslc_python_hello.json |
Hoists WSLc section. |
tests/configs/wslc_python_stdlib.json |
Hoists WSLc section. |
tests/configs/wslc_readonly_mount.json |
Hoists WSLc section. |
tests/configs/wslc_state_aware_provision.json |
Hoists lifecycle WSLc section. |
tests/configs/wslc_state_aware_provision_bridged.json |
Hoists lifecycle WSLc section. |
tests/configs/wslc_state_aware_provision_rejected_denied.json |
Hoists lifecycle WSLc section. |
tests/configs/wslc_state_aware_provision_rejected_hosts.json |
Hoists lifecycle WSLc section. |
tests/configs/wslc_state_aware_provision_rejected_proxy.json |
Hoists lifecycle WSLc section. |
tests/configs/wslc_state_aware_provision_with_filesystem.json |
Hoists lifecycle WSLc section. |
tests/configs/wslc_stderr.json |
Hoists WSLc section. |
tests/configs/wslc_tar_import_docker_save.json |
Hoists WSLc section. |
tests/configs/wslc_tar_import_rootfs.json |
Hoists WSLc section. |
tests/configs/wslc_timeout.json |
Hoists WSLc section. |
tests/examples/wslc_hello_world.json |
Updates WSLc example. |
tests/policy/README.md |
Documents shared fixtures. |
tests/policy/request-directional-network.json |
Adds request golden. |
tests/policy/request-process-container.json |
Adds request golden. |
tests/policy/request-wslc.json |
Adds WSLc request golden. |
tests/policy/state-aware-wslc-exec.json |
Adds WSLc exec golden. |
tests/policy/state-aware-wslc-provision.json |
Adds WSLc provision golden. |
tests/scripts/run_bwrap_all_tests.sh |
Registers new Bubblewrap tests. |
tests/scripts/run_bwrap_directional_test.sh |
Tests network budget rejection. |
tests/scripts/run_bwrap_readonly_denial_test.sh |
Tests read-only enforcement. |
tests/scripts/run_bwrap_teardown_test.sh |
Tests process-tree teardown. |
tests/scripts/run_bwrap_version_gate_test.sh |
Tests bwrap version gating. |
tests/scripts/run_lxc_all_tests.sh |
Updates LXC suite. |
tests/scripts/run_wslc_all_tests.ps1 |
Removes WSLc experimental flag. |
tests/scripts/run_wslc_denied_masking_test.ps1 |
Removes experimental flag. |
tests/scripts/run_wslc_dotdot_alias_test.ps1 |
Removes experimental flag. |
tests/scripts/run_wslc_most_specific_test.ps1 |
Removes experimental flag. |
tests/scripts/run_wslc_object_test.ps1 |
Removes experimental flag. |
tests/scripts/run_wslc_proxy_test.ps1 |
Removes experimental flag. |
tests/scripts/run_wslc_state_aware_tests.ps1 |
Removes lifecycle opt-in. |
Suppressed comments (1)
sdk/dotnet/README.md:540
- This newly added overview contradicts the promotion: WSLc is no longer experimental, while IsolationSession and Windows Sandbox remain experimental. Distinguish WSLc from those two so managed SDK users do not retain an unnecessary opt-in.
state-aware lifecycle supports IsolationSession, Windows Sandbox, and WSLC on
Windows; all three are experimental).
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| fn wslc_section_is_introduced_in_v09() { | ||
| assert_v09_introduces( |
| if (facts.wslcAvailable === true) { | ||
| support.availableMethods.push('wslc'); |
The promotion left three consumers still speaking the pre-promotion wire shape, which surfaced as four CI failures across two root causes. `MxcLifecycle.SetBackendConfig` nested every backend's per-phase config under `experimental.<backend>.<phase>` unconditionally. Now that the parser rejects `experimental.wslc` outright, that produced a `malformed_request` before backend resolution could report anything useful. Give the C# SDK the same section-root switch the TypeScript SDK already carries in `BACKEND_SECTION_ROOT`, so a promoted backend writes a closed top-level `wslc.<phase>` section while the still-experimental backends keep their `experimental` nesting. The switch is exhaustive over the known backends and throws on an undeclared one, so adding a backend without choosing a root fails loudly instead of silently defaulting. That single emitter fix clears three of the four failures: the .NET `WslcBuildSwitch_MatchesNativeAvailabilityAndStagesRuntimeUnit` test on all three platforms, and the Rust `managed_state_aware_goldens_are_accepted_by_native_contract` test in `mxc_ffi`, which feeds the C# golden fixtures back through the native contract. The golden `state-aware-wslc-provision.json` and the `BuildProvisionEnvelope_WslcUsesV08AndNestsImageOptions` assertion move with it; the latter now also asserts that no `experimental` section is emitted at all, so a regression cannot pass by nesting the section twice. Separately, `check-dotnet-api-parity.js` compared the managed `StateAwareContainment` enum against the `require_experimental_optin` list in `state_aware.rs`. That list is the *experimental subset*, not the set of state-aware backends; the two were only coincidentally equal before this promotion and diverge the moment a backend is promoted. Compare against `backend_from_prefix` in `state_aware_dispatch.rs`, which is the actual registry of reachable state-aware backends. This also strengthens the check, since it now tracks the dispatcher rather than a gate that shrinks over time, and it drops a now-redundant second read of `state_aware_dispatch.rs`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9f02fa89-c89e-4a5d-b80d-5130252d3654
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 109 out of 111 changed files in this pull request and generated 6 comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
sdk/node/src/platform.ts:171
- This availability fact is obtained by probing the auto-discovered
wxc-exec, butresolveExecutableAndArgsmay later runoptions.executablePath. A caller supplying a WSLc-enabled custom binary is therefore rejected when the separately discovered binary lacks WSLc (and the inverse can be incorrectly accepted). Probe the selected executable, or defer this per-binary feature check to an explicitly supplied executable.
if (facts.wslcAvailable === true) {
support.availableMethods.push('wslc');
sdk/node/tests/unit/sandbox.test.ts:1292
- This assertion passes only when the current host reports WSLc unavailable. On a WSLc-capable host the helper correctly returns arguments, so the test fails. Bypass the unrelated availability check and directly verify that promotion removed the CLI flag.
assert.throws(
() => resolveExecutableAndArgs(makeConfig('wslc'), { executablePath: fakeExe }),
(e: Error) => !/experimental mode/.test(e.message),
);
sdk/node/README.md:145
- WSLc does not work out of the box: default Windows builds omit the
wslcfeature and the README elsewhere requiresbuild.bat --with-wslc. Describe it as non-experimental but build-time opt-in.
The default `processcontainer`, `bubblewrap`, `lxc`, `seatbelt`, and `wslc` backends work out of the box. **Experimental backends** (`windows_sandbox`, `microvm`, `isolation_session`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend).
| // instead accept `provision` and reject the one-shot-only siblings, which | ||
| // the daemon-backed lifecycle does not honor, rather than silently | ||
| // dropping them. | ||
| if let Some(wslc) = cfg.wslc.as_ref() { |
There was a problem hiding this comment.
[AgentAssisted] Following up in case this was resolved in a batch — unchanged at 17d27c2f. Same root cause as the section_path() thread on models.rs; reasoning and suggested fix are there to keep it in one place.
| /// State-aware provision-phase configuration (`wslc.provision`). Carries | ||
| /// the container-creation knobs for the state-aware lifecycle; the flat | ||
| /// sibling fields above remain the one-shot surface. Absent on one-shot | ||
| /// configs and non-provision phases. |
There was a problem hiding this comment.
[AgentAssisted] Following up in case this was resolved in a batch — the behaviour looks unchanged at 17d27c2f.
wire::Wslc carries provision, so deny_unknown_fields doesn't reject it on a one-shot config, and convert_wire_config's WSLc block reads targetOs, image, imageTarPath, cpuCount, memoryMb, gpu, storagePath, portMappings — not provision. With WslcConfig::default().image = alpine:latest, a one-shot {"wslc":{"provision":{"image":"my-image:latest"}}} parses and runs Alpine.
The exact 0.9 contract already gets this right — one-shot Wslc in stable.rs has no provision field — so it's only reachable while the rolling parser is authoritative.
Happy to defer if there's a reason to leave it; otherwise rejecting provision in the one-shot converter closes it.
| ContainmentBackend::Lxc => Some("lxc"), | ||
| ContainmentBackend::WindowsSandbox => Some("experimental.windows_sandbox"), | ||
| ContainmentBackend::Wslc => Some("experimental.wslc"), | ||
| ContainmentBackend::Wslc => Some("wslc"), |
There was a problem hiding this comment.
[AgentAssisted] Following up in case this was resolved in a batch — both directions look unchanged at 17d27c2f.
matching_key is derived by section_path().strip_prefix("experimental."). At the merge base Wslc.section_path() was Some("experimental.wslc"), so a foreign key was rejected; it's now Some("wslc"), so matching_key is None and the None => return Ok(()) arm accepts a single foreign key, which cfg.experimental = None then drops. The reverse is thread 5: the cfg.wslc block in convert_wire_state_aware checks phase and one-shot-only fields but not the selected containment.
Neither direction mis-provisions anything — containment still wins and its policy is unaffected. The cost is that a malformed two-backend config is now accepted silently instead of rejected.
Worth noting this is per-promotion rather than WSLc-specific: matching_key is None for any containment whose section_path() has no experimental. prefix, so isolation_session will hit the same blind spot when it's promoted. Fixing the derivation rather than the instance would settle it once.
One-shot is unaffected — present_backend_sections / validate_single_backend_section were updated correctly.
| assert.throws( | ||
| () => spawnSandboxFromConfig(config), | ||
| { message: /experimental mode/ }, | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw from spawnSandboxFromConfig when experimental is not set', () => { | ||
| const config = createConfigFromPolicy({ version: '0.6.0-alpha' }, 'wslc'); | ||
| config.process!.commandLine = 'echo hello'; | ||
| assert.throws( | ||
| () => spawnSandboxFromConfig(config), | ||
| { message: /experimental mode/ }, | ||
| (e: Error) => !/experimental mode/.test(e.message), | ||
| ); |
| - Experimental backends: [`schemas/dev/`](https://github.com/microsoft/mxc/tree/main/schemas/dev/) | ||
|
|
||
| Open the schema file matching your `policy.version` (e.g. `mxc-config.schema.0.6.0-alpha.json`) and look up `processContainer`, `lxc`, `experimental.wslc`, `experimental.windows_sandbox`, etc. | ||
| Open the schema file matching your `policy.version` (e.g. `mxc-config.schema.0.6.0-alpha.json`) and look up `processContainer`, `lxc`, `wslc`, `experimental.windows_sandbox`, etc. |
|
|
||
| # Run a Linux command via MXC (requires --experimental) | ||
| wxc-exec.exe --experimental --debug wslc-config.json | ||
| # Run a Linux command via MXC (needs a build with the `wslc` Cargo feature) |
There was a problem hiding this comment.
Cargo has a wslc feature?
| }, | ||
| "memoryMb": { | ||
| "description": "Requested memory limit in megabytes.", | ||
| "maximum": 18446744073709551615, |
There was a problem hiding this comment.
note: noticed this just now, do we cap this anywhere? I assume there is a limit WSLc has under the hood
There was a problem hiding this comment.
That is a valid point.
Checked the SDK API reference (WslcSetSessionSettingsMemory), I don't think it caps anything.
The only real ceiling is the ABI: the setter takes uint32_t, so u32::MAX MB. We already enforce exactly that in the runner before the call. So keeping as-is.
One thing worth knowing: 0 isn't "no memory" — the SDK treats it as a sentinel that resets to the default 2000 MB (cpuCount: 0 likewise resets to 2). We land on the same behavior either way, since omitting the field skips the setter entirely.
| } | ||
| ], | ||
| "description": "WSL container backend config." | ||
| "description": "WSL container backend config (pre-promotion alias). Promoted to the top-level `wslc` section; still parsed here so the parser can reject it with a migration message instead of silently ignoring it." |
There was a problem hiding this comment.
question: I'm now realizing that we have an alpha and a dev and now I'm wondering if the promotion process is now that you do the same thing you did in the alpha, in the dev?
| // The managed `StateAwareContainment` enum must cover every backend the | ||
| // state-aware dispatcher can reach, which is the sandbox-id prefix registry in | ||
| // `state_aware_dispatch.rs` — NOT the `require_experimental_optin` list in | ||
| // `state_aware.rs`, which is only the experimental subset and shrinks as | ||
| // backends are promoted to the stable surface. | ||
| const rustPrefixBody = namedBody(rustDispatch, "fn", "backend_from_prefix"); | ||
| const rustBackends = [ | ||
| ...rustPrefixBody.matchAll(/=>\s*Ok\(ContainmentBackend::(\w+)\)/g), | ||
| ].map((match) => match[1]); | ||
| const managedStateAware = read( | ||
| "sdk", | ||
| "dotnet", | ||
| "Microsoft.Mxc.Sdk", | ||
| "StateAwareTypes.cs" | ||
| ); | ||
| compare( | ||
| "state-aware containment enum", | ||
| enumVariants(managedStateAware, "StateAwareContainment", "csharp"), | ||
| rustBackends | ||
| ); | ||
|
|
There was a problem hiding this comment.
note: currently confused with the content of this file tbh. That said, did it need to change for the promotion of wslc? It looks almost the same as what it was prior, if I take into account the deletion + additions
| // WSLc is promoted to the stable surface, so its phase section is a | ||
| // closed top-level `wslc.<phase>` object rather than nested under | ||
| // `experimental`. The parser rejects the old shape outright. |
There was a problem hiding this comment.
issue: comment not needed here, also do we need the check for experimental if we're removing it in this PR?
| private static bool IsExperimentalSection(string backend) => backend switch | ||
| { | ||
| IsolationSessionContainment => true, | ||
| WindowsSandboxContainment => true, | ||
| WslcContainment => false, | ||
| _ => throw new MxcException( | ||
| ErrorCode.UnsupportedContainment, | ||
| $"no wire section root declared for state-aware backend '{backend}'"), | ||
| }; |
There was a problem hiding this comment.
note: still wondering if this is needed since wslcContainment is already out of the experimental section with this PR. The other things might be nice to have but just trying to keep it scoped.
There was a problem hiding this comment.
Needed. The switch isn't WSLC-specific routing — IsolationSessionContainment and WindowsSandboxContainment still return true and require experimental.<backend>.<phase>. Deleting it breaks both. The WslcContainment => false arm is also load-bearing: without it the _ => arm throws UnsupportedContainment for wslc.
| .cloned() | ||
| .unwrap_or_default(); | ||
| let _ = writeln!(logger, "Using WSLContainer runner"); | ||
| let wslc_config = request.wslc.as_ref().cloned().unwrap_or_default(); |
There was a problem hiding this comment.
issue: thinking about about the unwrap_or_default(); line, shouldn't we actually return an error if we can't unwrap the users config rather than using a default?
There was a problem hiding this comment.
There's no failed unwrap to catch. request.wslc is None only when the section was omitted (in config_parser)- a malformed section already errors earlier. And when the section is present, the parser starts from WslcConfig::default() and overlays supplied fields. So unwrap_or_default() makes an absent section identical to "wslc": {}; erroring would let {} pass while omission fails.
| // Same story for WSLc: `appcontainer_common` cannot see that backend | ||
| // either. WSLc is promoted (non-experimental), so the SDK's | ||
| // availability check is the only gate on a `wslc` request — an | ||
| // un-overridden `false` here would make the backend unreachable. |
There was a problem hiding this comment.
issue: what's the same story? You mean the comment above with isolation session? Also we can probably remove the promotion text.
Another thing for us to think about in the future is that these files are getting away from us. It's all getting pretty tangled. Nothing your PR can fix so it's not your PRs fault I'm gonna start thinking about what we can do in a refactoring effort.
| /// to the permissive `experimental.<BACKEND_KEY>` block; a backend promoted | ||
| /// to the stable surface overrides this to [`SectionRoot::Stable`] so its | ||
| /// config is read from the top-level `<BACKEND_KEY>` section instead. | ||
| const SECTION_ROOT: SectionRoot = SectionRoot::Experimental; |
There was a problem hiding this comment.
note: calling adpa-ms to confirm sessions is still ok here, as he's also adding state-aware stuff at the same time and wanting to make sessions stable soon.
There was a problem hiding this comment.
[AgentAssisted] Sessions are fine as-is. SECTION_ROOT defaults to Experimental and IsolationSessionRunner doesn't override it, so iso dispatch and its experimental.isolation_session.<phase> path are unchanged by this PR.
Two things worth tightening before sessions are promoted:
-
The default means a backend promoted later that forgets to override it silently keeps the old root — and a mismatch fails quietly, since
deserialize_configreturnsOk(None)for a missing key and the backend then applies defaults. Dropping the default (one line in three impls, three stubs) makes it a compile error instead. Same shape as yourunwrap_or_default()note onrun.rs. -
The root is declared in three places — here,
BACKEND_SECTION_ROOT, andIsExperimentalSection— with no parity check;check-dotnet-api-parity.jscovers the enum and id prefixes but not the root.
| config.wslc = { | ||
| image: 'alpine:latest', |
There was a problem hiding this comment.
[AgentAssisted] This builder's output is always rejected by native WSLc: createConfigFromPolicy synthesizes config.ui unconditionally, config_parser sets policy.ui_specified = cfg.ui.is_some(), and wslc_common::policy::reject_ui_policy refuses any supplied ui before a container starts. Every spawnSandbox / spawnSandboxFromConfig WSLc call therefore fails, including one with no ui in the policy.
mxc_engine::policy already handles this — it emits ui only when the caller supplied one.
Missed by validation because sdk/node/tests/integration/wslc-e2e.test.ts is gated on MXC_ENABLE_WSLC_TESTS=1 and no WSLc fixture sets ui.
Predates this PR — the rejection arrives with the base branch — so the fix may belong there. Raising it here because this is the PR that declares WSLc stable.
Suggested: drop config.ui here when policy.ui is absent, and keep an explicitly supplied one so native validation can still reject it. Test both cases.
|
[AgentAssisted] Two documentation spots a comment sweep may not reach — both outside the current diff, so not attachable to a line.
|
The merge-base changed after approval.
📖 Description
Closes #1049
111 files changed — 58 test, 53 non-test (42 source/config, 11 docs).
WSLc is now configured through the top-level
wslcsection instead ofexperimental.wslc, and selecting"containment": "wslc"no longer requires--experimental/{ experimental: true }/SandboxRequest::set_experimental(true). This mirrors the Seatbelt promotion precedent.Wire + schema
wslcfromwire::Experimentalto theMxcConfigroot, and fromexperimental.rstostable.rsin the 0.9.0-alpha closed contract.experimental.wslcas a rejection alias so a pre-promotion config fails with an actionable migration message rather than being silently ignored.Parser + dispatch
SectionRoot(Experimental|Stable);StatefulSandboxBackend::SECTION_ROOTdefaults toExperimentaland onlyWslcStateAwareRunneroverrides it.ParsedStateAwareRequestgainsstable_rawfor stable-rooted backends.TypeScript SDK
wslcout ofContainerConfig.experimentaland offExperimentalBackends; addBACKEND_SECTION_ROOT, the TS twin of the RustSectionRoot, so envelopes are built at the right root.wslcAvailableprobe fact end to end (Rust probe -> engine ->wxc-exec --probe-> SDK). Without it, removing the experimental gate unmasked the fact thatwslcwas never inavailableMethods, which would have made the backend unreachable through the SDK.Rust SDK, corpus, harnesses and docs
set_experimental(true)requirement frommxc-sdk.experimental.wslcto top-levelwslcin 31 test configs.--experimentalfrom the WSLc test harnesses..github/copilot-instructions.md.🔗 References
🔍 Validation
Unit / static:
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --features wxc/wslc -- -D warningscargo test -p wxc_common -p wslc_common -p mxc_engine -p wxc -p mxc-sdk --features wxc/wslc,mxc-sdk/wslc— 1,346 testscargo test -p mxc_config_contract --features schema-gen— 707 testscd sdk/node && npm test— 283 tests, 0 failcheck-schema-versions,check-schema-codegen,check-sdk-types-codegen,check-contract-codegen,validate-configs(267 configs),check-version-sync,check-rust-toolchain-sync,check-psec-codegen,check-dotnet-errorcode-parityLive WSLc E2E on a WSL2 host (
run_wslc_all_tests.ps1, which chains the per-scenario scripts and delegates to the state-aware suite): 78/79. The 3 reported assertion failures are allnetwork.proxypost-provision error-code mismatches that are pre-existing on the base commit — verified by rebuilding at the merge-base and reproducing byte-identical output — and are fixed independently by #1042.Also verified end to end against a real
wxc-exec.exebuild: a promoted top-levelwslcconfig parses and runs a container to completion, the pre-promotionexperimental.wslcshape produces the migration error, and--probereportswslcAvailable: true.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
GitHub Actions runs the PR validation build automatically. The ADO pipeline
(
MXC-PR-Build) is the Azure version of the PR pipeline, kept in parity with the GitHubActions build; it runs on merge to
main, and Microsoft reviewers with write access can trigger iton a PR with
/azp run. See docs/pull-requests.md.If the
dependency-feed-checkcheck fails on a new dependency, the crate must be added tothe feed before the PR can pass. See docs/pull-requests.md
for the steps.
Microsoft Reviewers: Open in CodeFlow