Skip to content

[WSLC] Promote from experimental to the stable config surface - #1064

Open
Soham Das (SohamDas2021) wants to merge 8 commits into
mainfrom
user/sodas/wslc-promote-to-stable
Open

[WSLC] Promote from experimental to the stable config surface#1064
Soham Das (SohamDas2021) wants to merge 8 commits into
mainfrom
user/sodas/wslc-promote-to-stable

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

📖 Description

Closes #1049

111 files changed — 58 test, 53 non-test (42 source/config, 11 docs).

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.

🔗 References

🔍 Validation

Unit / static:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --features wxc/wslc -- -D warnings
  • cargo test -p wxc_common -p wslc_common -p mxc_engine -p wxc -p mxc-sdk --features wxc/wslc,mxc-sdk/wslc — 1,346 tests
  • cargo test -p mxc_config_contract --features schema-gen — 707 tests
  • cd sdk/node && npm test — 283 tests, 0 fail
  • All gates: check-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-parity

Live 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 all network.proxy post-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.exe build: a promoted top-level wslc config parses and runs a container to completion, the pre-promotion experimental.wslc shape produces the migration error, and --probe reports wslcAvailable: true.

✅ Checklist

📋 Issue Type

  • Bug fix
  • Feature
  • Task

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 GitHub
Actions build; it runs on merge to main, and Microsoft reviewers with write access can trigger it
on a PR with /azp run. See docs/pull-requests.md.

If the dependency-feed-check check fails on a new dependency, the crate must be added to
the feed before the PR can pass. See docs/pull-requests.md
for the steps.

Microsoft Reviewers: Open in CodeFlow

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
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner August 28, 2026 18:48
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@microsoft-github-policy-service microsoft-github-policy-service Bot added the Copilot-Instructions PR modifies Copilot instruction files (.github/copilot-instructions.md or .github/instructions/) label Aug 28, 2026
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Copilot AI review requested due to automatic review settings August 28, 2026 20:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 105 out of 107 changed files in this pull request and generated 3 comments.

Comment thread src/core/wxc_common/src/wire.rs
Comment on lines +7 to +8
fn wslc_section_is_introduced_in_v09() {
assert_v09_introduces(
Comment thread sdk/node/src/platform.ts
Comment on lines +170 to +171
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
Copilot AI review requested due to automatic review settings August 28, 2026 20:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, but resolveExecutableAndArgs may later run options.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 wslc feature and the README elsewhere requires build.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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +657 to +660
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines 1135 to 1138
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),
);
Comment thread sdk/node/README.md
Comment thread sdk/node/README.md
- 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cargo has a wslc feature?

@SohamDas2021 Soham Das (SohamDas2021) changed the title [WSLC] Promote WSLc from experimental to the stable config surface [WSLC] Promote from experimental to the stable config surface Sep 1, 2026
},
"memoryMb": {
"description": "Requested memory limit in megabytes.",
"maximum": 18446744073709551615,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: noticed this just now, do we cap this anywhere? I assume there is a limit WSLc has under the hood

@SohamDas2021 Soham Das (SohamDas2021) Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines +453 to +473
// 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
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +427 to +429
// 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: comment not needed here, also do we need the check for experimental if we're removing it in this PR?

Comment on lines +504 to +512
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}'"),
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/core/wxc/src/main.rs
Comment on lines +785 to +788
// 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

  1. The default means a backend promoted later that forgets to override it silently keeps the old root — and a mismatch fails quietly, since deserialize_config returns Ok(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 your unwrap_or_default() note on run.rs.

  2. The root is declared in three places — here, BACKEND_SECTION_ROOT, and IsExperimentalSection — with no parity check; check-dotnet-api-parity.js covers the enum and id prefixes but not the root.

Comment thread src/core/wxc_common/src/wire.rs
Comment thread src/core/wxc_common/src/config_parser.rs
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs-Author-Feedback Issue needs attention from issue or PR author Needs-Attention Issue needs attention from Microsoft and removed Needs-Author-Feedback Issue needs attention from issue or PR author labels Sep 3, 2026
Comment thread sdk/node/src/sandbox.ts
Comment on lines +120 to +121
config.wslc = {
image: 'alpine:latest',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@adpa-ms

adpa-ms commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

[AgentAssisted] Two documentation spots a comment sweep may not reach — both outside the current diff, so not attachable to a line.

wire.rs's Containment::Wslc is still documented /// WSL container (experimental)., and that string is emitted as the description for the wslc containment value in the regenerated 0.9.0-dev schema, so config authors see "experimental" in editor validation. The codegen gate will catch the regeneration once the doc comment is updated.

docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md still contains the literal experimental.<backend> 16 times, with no mention of SectionRoot / SECTION_ROOT / stable_raw / BACKEND_SECTION_ROOT. As the cross-backend contract doc it's now wrong for one of its three backends, and it's the doc the next promotion would follow.

Base automatically changed from user/sodas/wslc-rejection-hardening to main September 4, 2026 00:07
@SohamDas2021
Soham Das (SohamDas2021) dismissed Darren Hoehna (dhoehna)’s stale review September 4, 2026 00:07

The merge-base changed after approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Copilot-Instructions PR modifies Copilot instruction files (.github/copilot-instructions.md or .github/instructions/) Needs-Attention Issue needs attention from Microsoft

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[WSLC] Promote WSLC, move it out of experimental

5 participants