[WSLC] State-aware sandbox lifecycle over the experimental.wslc surface (PR 2b/3) - #801
[WSLC] State-aware sandbox lifecycle over the experimental.wslc surface (PR 2b/3)#801Soham Das (SohamDas2021) wants to merge 5 commits into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds daemon-backed, state-aware WSLc lifecycle support across Rust dispatch, wire schema, documentation, and E2E coverage.
Changes:
- Implements WSLc provision/start/exec/stop/deprovision dispatch and policy validation.
- Extends generated schema and wire types with WSLc provision configuration.
- Adds lifecycle fixtures, E2E harness, daemon timeout controls, and documentation.
Reviewed changes
Copilot reviewed 30 out of 31 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
.github/copilot-instructions.md |
Documents WSLc architecture and tests. |
docs/wsl/wslc-state-aware.md |
Describes lifecycle behavior and limitations. |
schemas/dev/mxc-config.schema.0.8.0-dev.json |
Adds generated WSLc provision schema. |
sdk/node/src/generated/wire.ts |
Adds generated provision wire types. |
src/backends/wslc/common/src/daemon_client.rs |
Adds typed errors and daemon spawning changes. |
src/backends/wslc/common/src/lib.rs |
Exports state-aware modules. |
src/backends/wslc/common/src/policy.rs |
Implements per-phase policy validation. |
src/backends/wslc/common/src/state_aware.rs |
Implements the state-aware backend. |
src/backends/wslc/daemon/src/main.rs |
Adds configurable idle timing. |
src/core/mxc_engine/src/state_aware.rs |
Wires WSLc into engine dispatch. |
src/core/wxc_common/src/state_aware_dispatch.rs |
Registers the wslc ID prefix. |
src/core/wxc_common/src/wire.rs |
Adds WSLc provision wire configuration. |
tests/configs/wslc_state_aware_stop.json |
Adds stop fixture. |
tests/configs/wslc_state_aware_start.json |
Adds start fixture. |
tests/configs/wslc_state_aware_provision.json |
Adds basic provision fixture. |
tests/configs/wslc_state_aware_provision_with_filesystem.json |
Adds volume fixture. |
tests/configs/wslc_state_aware_provision_rejected_proxy.json |
Tests provision-time proxy rejection. |
tests/configs/wslc_state_aware_provision_rejected_hosts.json |
Tests host-filter rejection. |
tests/configs/wslc_state_aware_provision_rejected_denied.json |
Tests denied-path rejection. |
tests/configs/wslc_state_aware_provision_bridged.json |
Adds bridged-network fixture. |
tests/configs/wslc_state_aware_exec_write_marker.json |
Writes warm-reuse marker. |
tests/configs/wslc_state_aware_exec_rejected_filesystem.json |
Tests immutable filesystem policy. |
tests/configs/wslc_state_aware_exec_read_marker.json |
Reads warm-reuse marker. |
tests/configs/wslc_state_aware_exec_proxy.json |
Tests proxy environment injection. |
tests/configs/wslc_state_aware_exec_exit_7.json |
Tests exit code 7. |
tests/configs/wslc_state_aware_exec_exit_1.json |
Tests exit code 1. |
tests/configs/wslc_state_aware_exec_exit_0.json |
Tests successful exit. |
tests/configs/wslc_state_aware_exec_env.json |
Tests per-exec environment. |
tests/configs/wslc_state_aware_exec_basic.json |
Adds basic exec fixture. |
tests/configs/wslc_state_aware_deprovision.json |
Adds deprovision fixture. |
tests/scripts/run_wslc_state_aware_tests.ps1 |
Adds multi-invocation E2E harness. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
69876f6 to
499f351
Compare
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
src/backends/wslc/common/src/policy.rs:124
- This value-based check silently accepts an explicitly supplied
network.defaultPolicy: "block"on post-provision phases, and it also misses explicit default-valued fields such asallowLocalNetwork: falseorenforcementMode: "capabilities". Those settings are not applied after provision, despite the documented immutable-policy contract. Start/stop/deprovision can rejectnetwork_specified; exec needs field-level presence tracking so a proxy-only block remains allowed while all other network fields are rejected.
fn reject_non_default_network(request: &ExecutionRequest) -> Result<(), MxcError> {
if request.policy.default_network_policy != NetworkPolicy::Block {
return Err(MxcError::policy_validation(ERR_NETWORK_IMMUTABLE));
}
src/backends/wslc/common/src/state_aware.rs:270
- A missing WSLC runtime is reported by the daemon as
WorkerError::Backend/ErrKind::Backend, which this maps tobackend_error. This contradicts the documentedbackend_unavailablecontract and means the E2E availability probe only skips feature-off builds; a host without the runtime continues into the suite and fails every lifecycle test. Add a distinct unavailable/not-ready classification for SDK load/prerequisite failures and map it tobackend_unavailable.
DaemonError::Daemon { kind, message } => match kind {
ErrKind::NotProvisioned => MxcError::not_provisioned(message),
ErrKind::NotStarted => MxcError::not_started(message),
ErrKind::Busy | ErrKind::NotReady | ErrKind::Protocol | ErrKind::Backend => {
MxcError::backend_error(message)
}
},
DaemonError::Transport(e) => MxcError::backend_error(format!("{e:#}")),
tests/scripts/run_wslc_all_tests.ps1:342
- The delegated harness exits 0 both when it passes and when it skips because the daemon/runtime is unavailable, but this always records
Skipped = $falseand counts either case as a pass. As a result, the advertised single entry point can report all coverage passing without running any state-aware tests. Preserve a distinct skip result from the child harness and record it here.
499f351 to
f96a091
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
src/backends/wslc/common/src/policy.rs:123
- This value-only check cannot distinguish an absent network section from an explicit
defaultPolicy: "block". A sandbox provisioned asallow/Bridged therefore accepts a later exec requestingblockbut stays bridged, silently violating the requested restriction. Preserve presence for the network-mode fields (separately from a proxy-only network section) and reject any post-provision mode request.
fn reject_non_default_network(request: &ExecutionRequest) -> Result<(), MxcError> {
if request.policy.default_network_policy != NetworkPolicy::Block {
return Err(MxcError::policy_validation(ERR_NETWORK_IMMUTABLE));
src/backends/wslc/common/src/policy.rs:57
- Provision validation accepts
network.allowLocalNetwork=true, butmap_networkconsumes onlydefault_network_policy; no inbound-access or port-forward setting is applied. The one-shot WSLc path explicitly rejects this unsupported request (wsl_container_runner.rs:646-650), so state-aware provision should also fail rather than silently claiming the policy is honored.
This issue also appears on line 121 of the same file.
reject_host_filtering(request)?;
src/backends/wslc/common/src/state_aware.rs:268
- Missing WSLC runtime components fail inside
load_sdk_checked/session creation as daemonErrKind::Backend, so this maps them tobackend_error. That contradicts the documentedbackend_unavailablecontract and prevents the new E2E availability probe (which only skipsbackend_unavailable) from skipping unsupported hosts. Add a distinct daemon classification for runtime unavailability and map it accordingly.
ErrKind::Busy | ErrKind::NotReady | ErrKind::Protocol | ErrKind::Backend => {
MxcError::backend_error(message)
}
src/core/wxc_common/src/wire.rs:527
- This adds a configuration field to the generated schema, but the canonical schema reference still documents only the flat one-shot
experimental.wslcshape. The repository convention requires schema additions to updatedocs/schema.md; add the nested state-awareprovisionshape there so callers can discoverimageandimageTarPath.
/// State-aware provision-phase configuration
/// (`experimental.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.
pub provision: Option<WslcProvisionPhase>,
f96a091 to
f311b6c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
src/backends/wslc/common/src/state_aware.rs:73
- State-aware provisioning maps the filesystem policy directly to mounts, bypassing the object-alias normalization and delegation checks used by the one-shot WSLc path (
wsl_container_runner.rs:1282-1309). As a result, the same host object can be exposed read-write through one alias despite being read-only through another, and paths are mounted without the established caller-access check. Normalize/tighten the policy and runcheck_delegationbefore overlap validation and volume construction.
let image_tar_path = config.and_then(|c| c.image_tar_path);
let volumes = build_daemon_volumes(request)?;
tests/scripts/run_wslc_all_tests.ps1:342
- The delegated script exits 0 for prerequisite skips (for example, a missing daemon or
backend_unavailable), so this records a skipped state-aware suite asPass = true, Skipped = false. The aggregate summary then claims the lifecycle coverage passed when none ran. Propagate a distinct skip result or perform the prerequisite probe here and setSkippedaccurately.
src/backends/wslc/common/src/policy.rs:58 - The state-aware validator never rejects
network.allowLocalNetwork=true, althoughmap_networkignores it and the one-shot WSLc validator explicitly rejects it (wsl_container_runner.rs:646-650). Provision parsing catches this in the CLI path, but post-provision requests resolve by sandbox ID and direct engine callers bypass that parser guard, so the unsupported grant can be silently accepted. Apply the same rejection in the backend validation for every phase.
reject_host_filtering(request)?;
if request.policy.network_proxy.is_enabled() {
src/core/wxc_common/src/wire.rs:527
- This adds a public config field, but the canonical schema reference in
docs/schema.mdstill shows only the flat one-shotexperimental.wslcfields and omits the nestedprovisionshape. Update that reference so state-aware users can discover and author this field consistently with the generated schema.
/// State-aware provision-phase configuration
/// (`experimental.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.
pub provision: Option<WslcProvisionPhase>,
f311b6c to
f7802dc
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/backends/wslc/common/src/policy.rs:124
- Comparing only the mapped value cannot distinguish an omitted network mode from an explicit
defaultPolicy: "block". For example, after provisioning a bridged (allow) sandbox, an exec request that explicitly asks forblockpasses this check even though the container remains bridged, silently dropping a restrictive policy. Track presence of the mode field separately (while still allowing a proxy-only network block) and reject any post-provision mode request.
fn reject_non_default_network(request: &ExecutionRequest) -> Result<(), MxcError> {
if request.policy.default_network_policy != NetworkPolicy::Block {
return Err(MxcError::policy_validation(ERR_NETWORK_IMMUTABLE));
}
tests/scripts/run_wslc_state_aware_tests.ps1:176
ProcessStartInfo.ArgumentListis unavailable in Windows PowerShell 5.1's .NET Framework, and this Windows harness does not require PowerShell 7. The first invocation therefore throws before any lifecycle test runs on the default Windows shell. These arguments contain no spaces, so assigning the joined string keeps the harness compatible with both editions.
tests/scripts/run_wslc_all_tests.ps1:341- The child harness exits 0 both when it passes and when it skips because the daemon or backend is unavailable. Treating every zero as
Pass = trueand hard-codingSkipped = falsemakes the aggregate summary claim state-aware coverage passed when no state-aware test ran. Propagate a distinct skip status from the child (or otherwise report its prerequisite outcome) before adding this result.
src/backends/wslc/common/src/state_aware.rs:268 - A missing WSLC runtime is reported by the daemon worker as
ErrKind::Backendbecauseensure_sessionmapsload_sdk_checkedfailures toWorkerError::Backend; this arm therefore returnsbackend_error, not the documentedbackend_unavailable. As a result the new prerequisite probe does not skip a host without the runtime and the public error mapping contradicts this PR's documentation. Add a distinct unavailable classification through the worker/protocol/client mapping.
DaemonError::Daemon { kind, message } => match kind {
ErrKind::NotProvisioned => MxcError::not_provisioned(message),
ErrKind::NotStarted => MxcError::not_started(message),
ErrKind::Busy | ErrKind::NotReady | ErrKind::Protocol | ErrKind::Backend => {
MxcError::backend_error(message)
}
…lc surface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3ae1af2-7b79-4340-a5ce-a5402e7ede3d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3ae1af2-7b79-4340-a5ce-a5402e7ede3d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f3ae1af2-7b79-4340-a5ce-a5402e7ede3d
f7802dc to
20b01fc
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/backends/wslc/common/src/state_aware.rs:73
- Provision builds daemon mounts directly from the raw policy, bypassing the object-identity normalization and delegation check used by the one-shot WSLc path (
wsl_container_runner.rs:1282-1325). A caller can therefore mount an alias that should have been tightened to denied, or delegate a path the caller is not authorized to share. Normalize first, runcheck_delegationon the effective policy, and re-run overlap validation before constructingVolumeMounts; PR #806’s follow-up diff confirms this gap and the required ordering.
let volumes = build_daemon_volumes(request)?;
src/backends/wslc/common/src/policy.rs:126
- This post-provision network check misses
allowLocalNetwork. Forstart/exec/stop/deprovision, containment is resolved fromsandboxId, so the parser cannot apply its WSLc-specific rejection;allowLocalNetwork=truetherefore reaches this validator, passes whendefaultPolicyis Block, and is silently ignored. Reject it here as the one-shot WSLc validator does atwsl_container_runner.rs:646-650.
fn reject_non_default_network(request: &ExecutionRequest) -> Result<(), MxcError> {
if request.policy.default_network_policy != NetworkPolicy::Block {
return Err(MxcError::policy_validation(ERR_NETWORK_IMMUTABLE));
}
Ok(())
docs/wsl/wslc-state-aware.md:59
- The state-aware exec path does not forward stdin: it sends no stdin data and returns
null_pipe_handle()for stdin instate_aware.rs:195. The linked PR #806 also explicitly defers piped stdin to a later tier, so documenting it as supported will mislead callers.
| exec | `WslcCreateContainerProcess` in the warm container; stream stdout/stderr, forward stdin, return the process exit code. A timeout SIGKILLs the **process**, not the container. |
tests/scripts/run_wslc_all_tests.ps1:342
- The delegated harness exits 0 both when it passes and when it skips due to a missing daemon/backend, but this result is always recorded as
Skipped = $false. Consequently the aggregate summary counts an unexecuted state-aware suite as a passing test. Return a distinct skip status/result from the child and map it toSkipped = $truehere.
tests/scripts/run_wslc_state_aware_tests.ps1:314 - This probe does not actually catch a missing WSLc runtime as documented. The daemon becomes ready before loading the SDK; the first provision’s
load_sdk_checkedfailure is converted toWorkerError::Backend, thenErrKind::Backend, and finallybackend_error, so thisbackend_unavailable-only branch is skipped and the entire suite fails instead of skipping. Preserve an unavailable classification across the daemon protocol (without treating every backend error as a skip).
…network-mode changes by presence post-provision
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/backends/wslc/common/src/state_aware.rs:94
- Re-run denied-path overlap validation after object normalization. Normalization can move a mounted alias into
deniedPathswhile leaving its parent mount in place; because the only overlap check ran on the raw policy, the parent is then mounted and exposes the newly denied alias. The one-shot path deliberately validates again after normalization (wsl_container_runner.rs:1312-1327).
let volumes = build_daemon_volumes(request)?;
src/backends/wslc/common/src/policy.rs:57
- Provision currently accepts
network.allowLocalNetwork=true, butmap_networkonly mapsdefaultPolicy, so this requested network posture is silently ignored. The one-shot WSLc validator rejects this unsupported setting (wsl_container_runner.rs:646-650); state-aware provision should do the same rather than creating a sandbox with different exposure than requested.
reject_host_filtering(request)?;
tests/scripts/run_wslc_state_aware_tests.ps1:176
ProcessStartInfo.ArgumentListis unavailable in the .NET Framework used by the default Windows PowerShell 5.1, so this harness throws before launching its first phase unless run under PowerShell 7. The repository does not require PowerShell 7 and sibling harnesses useArguments; these arguments are flags plus base64, so joining them is safe and keeps the test runnable from Windows PowerShell.
src/core/mxc_engine/src/state_aware.rs:35- This branch also runs on non-Windows targets, where the message incorrectly claims the build lacks the
wslcfeature even when that feature is enabled. Report both supported conditions so Linux/macOS callers receive an accurate availability error.
"the WSLc backend is not available in this build (compiled without the `wslc` feature)",
src/core/wxc_common/src/wire.rs:541
- The wire format has no top-level
policysection; mounts and network mode come from top-levelfilesystemandnetwork. This incorrect wording is propagated into the generated schema and TypeScript file, whiledocs/schema.mdstill shows only the flat one-shot WSLc fields. Update the source wording, document the nested state-awareprovisionshape in the schema reference, and regenerate both artifacts.
/// Filesystem mounts and network mode derive from the top-level `policy`
/// section (readwrite / readonly paths, network), not from here. The
Summary
PR 2a/3 (#767) hardened the per-user WSLc daemon internals. This PR (2b/3) puts the
daemon to work: it wires the state-aware sandbox lifecycle (provision → start →
exec → stop → deprovision) through the public
experimental.wslc.*wire schema, so acaller can drive a long-lived WSLc container across multiple invocations instead of
one-shot run-to-completion.
No new public SDK types yet — that's PR 3/3. This PR lands the Rust backend, wire/schema
surface, engine dispatch, and the E2E harness.
Changes:
wslc/common/state_aware.rsimplementsStatefulSandboxBackend(dispatch prefix
wslc), translating each lifecycle phase into daemon protocol framesover the owner-only named pipe via
daemon_client.policy.rsmaps + validates theper-phase policy (filesystem/network/host rules honored at provision; rejected where the
backend can't enforce them).
Wslc*Phaseconfig added to the wire model(
wxc_common::wire); regenerated dev schema (schemas/dev/mxc-config.schema.0.8.0-dev.json)and generated TS wire types (
sdk/node/src/generated/wire.ts) — both codegen artifacts,not hand-edited.
mxc_enginestate-aware arm for WSLc, and thewslcsandbox-id prefix now resolves toContainmentBackend::Wslcinstate_aware_dispatch(with a unit test).main.rshandles the state-aware phase requests.tests/configs/wslc_state_aware_*.jsonfixtures (provision/start/exec/stop/deprovision plus rejection cases for denied paths, hosts, proxy, and filesystem)
and a multi-invocation
tests/scripts/run_wslc_state_aware_tests.ps1harness withwarm-reuse and idle-teardown assertions.
docs/wsl/wslc-state-aware.md(fixture ordering + sandbox-id substitution)and a
copilot-instructions.mdupdate.Testing
cargo clippy --workspace --all-targets -- -D warningsandcargo fmt --checkclean.wslc_commonunit tests green (incl. thewslc-prefix dispatch resolution test);the G8
notify_oneidle-wakeup regression test and the daemonresolve_durationenv-overridetests both pass.
build.bat --with-wslcsucceeds.run_wslc_all_tests.ps1) passing; the state-aware harness(
run_wslc_state_aware_tests.ps1) drives provision → start → exec → exec → stop → deprovisionon a WSL2 host.
Coming in the pipeline
live stdout/stderr from state-aware
execand adds the provision-time filesystemdelegation gate.
Wslc*Config/*Resulttypes + brandedSandboxId<'wslc'>and helper prefix wiring, mirroring the LXC state-aware SDK surface.
Let me clean up the temp file I created.
🔗 References
✅ Checklist
docs/wsl/wslc-state-aware.md)Cargo.lock, thedependency-feed-checkcheck passes📋 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