Skip to content

State-aware exec streaming + typed-error contract gaps (FFI in-process + CLI executor framing) #843

Closed

Description

Description of the task

Track and fix the state-aware exec streaming contract gaps surfaced as review comments on #806 and #810, plus a related typed-error gap surfaced on #1045 (defect 4). These are four distinct failure modes that share one theme — how exec output and typed errors are framed across the executor/library boundary. Defects 1-3 are best fixed together because they touch the same code; defect 4 is independent and can land on its own.

Defects 1-3 all still stand on main after #829 ("Relay live exec pipes to executor stdio"), which added the ExecConsumer (Executor vs Library) plumbing and a generic relay_exec_to_stdio, but did not update WSLc's exec (it takes _consumer: ExecConsumer and still returns null pipes) or change the executor's stdout error framing.

1. The require_experimental_optin gate blocks the in-process (FFI/Rust-SDK) path.
require_experimental_optin (src/core/mxc_engine/src/state_aware.rs:43,52,73,109) rejects the experimental backends (WSLc / IsolationSession / WindowsSandbox) unless request.experimental_enabled is set. The CLI sets this from --experimental, but the JSON/FFI entry points parse via convert_wire_state_aware → convert_wire_config, which hardcodes experimental_enabled = false (src/core/wxc_common/src/config_parser.rs:1335). mxc_sdk::exec_sandbox / mxc_state_aware_exec expose no opt-in. Result: every in-process SDK/FFI exec against a wslc: (or IsolationSession/WindowsSandbox) sandbox returns backend_unavailable before dispatch.

Fix (small, ~2 lines): set experimental_enabled = true on the trusted JSON/FFI entry points after parse (linking the native library is itself the opt-in), or thread an explicit opt-in through the SDK/FFI request path.

2. WSLc exec returns null pipe handles and blocks (the ExecConsumer::Library path).
WslcStateAwareRunner::exec (src/backends/wslc/common/src/state_aware.rs:135-206) relays the daemon stream synchronously into process-global stdout/stderr, blocks to exit, then returns an ExecHandle with null_pipe_handle() for stdout/stderr/stdin. Post-#829 it receives an ExecConsumer but ignores it (_consumer, line 140). Correct for the CLI/ExecConsumer::Executor path (self-relay + null pipes is the documented call-through), but the FFI path (state_aware_dispatch.rs:97 passes ExecConsumer::Library) wraps the handle in ExecSandboxProcess, which expects live pipes + non-blocking spawn — so mxc_state_aware_exec blocks until exit and take_stdout/take_stderr return None, contradicting the live MxcSandboxProcess contract.

Fix (moderate, ~150-250 lines): when consumer == ExecConsumer::Library, create real OS pipes, run exec_streaming on a background thread whose callback writes to the pipe write-ends, and return an ExecHandle carrying the read-ends + a waiter that joins the thread. Keep the ExecConsumer::Executor branch on the internal self-relay (or hand live pipes to the generic relay_exec_to_stdio #829 landed). Add stdin forwarding and handle-ownership care. IsolationSession and WindowsSandbox share the identical null-pipe pattern and should adopt the same shared helper.

3. Executor stdout error-framing collision on post-admission failures (ExecConsumer::Executor).
On the CLI/executor path, exec streams user bytes to stdout as they arrive. On a post-admission failure (timeout mid-output, daemon truncation, backend failure after some bytes streamed), exec_streaming returns Err, and run_state_aware_main finalizes via StateAwareExit::Error(json) → println!(envelope); exit(1) (src/core/wxc/src/main.rs:457,484) — appending the JSON error envelope to the same stdout that already carries user output. On the SDK side, execInSandboxAsync calls tryParseErrorEnvelope, which runs JSON.parse(stdout.trim()) over the whole buffer (sdk/node/src/state-aware-helper.ts:172-185); mixed [user output] + {"error":…} is not valid JSON → returns null → no typed MxcError is thrown → the call resolves with { exitCode: 1, stdout: <mixed> }. Pre-admission failures (bad policy, sandbox not found) still stream nothing and produce a clean envelope, so only post-admission failures are affected.

Fix (moderate; coordinated Rust + SDK): give the exec path separable framing for post-admission failures — e.g., route the exec error envelope to stderr (leaving user output as the sole owner of stdout) with the SDK reading the typed error from stderr, or length-prefix/frame the streams. Whatever is chosen must keep pre-admission envelopes working and keep the CLI human-readable.

4. The daemon protocol flattens every backend failure to backend_error, discarding its typed classification.
Surfaced as a review comment on #1045. #794 gave the WSLc backend a typed WslcError whose variants carry a FailurePhase (Unavailable → BackendUnavailable, Rejected → Rejected, etc.), so the one-shot / streaming surface now reaches the Rust SDK with an accurate code via mxc_engine::dispatch::map_spawn_error. The state-aware surface does not, because the classification is destroyed crossing the daemon's named pipe: sr_err (src/backends/wslc/daemon/src/session_manager.rs:59-61) reduces the step helper's ScriptResponse to anyhow!(resp.error_message), dropping failure_phase; that becomes WorkerError::Backend → ErrKind::Backend (:82); and map_daemon_error (src/backends/wslc/common/src/state_aware.rs:299-301) maps ErrKind::Backend to MxcError::backend_error.

Concretely: provisioning on a host with missing WSLc components returns backend_error to state-aware Rust SDK callers, where the one-shot surface returns backend_unavailable. There is no local preflight that could catch it earlier — state_aware.rs has no ensure_host_supported / load_sdk_checked call, and its only backend_unavailable (connect_daemon) covers an unreachable daemon, a different condition. So a caller still has to string-match the message to distinguish an unusable host from a rejected policy.

Fix (moderate; daemon wire change): widen ErrKind with Unavailable and Rejected rather than plumbing FailurePhase through — failure_phase is a ScriptResponse concept that the state-aware boundary does not use, whereas map_daemon_error already maps ErrKind 1:1 onto MxcError codes, so the client side is a new match arm. Requires: a PROTOCOL_VERSION bump, somewhere on WorkerError to carry the classification so sr_err stops discarding it, skew handling for an already-running persistent daemon speaking the old protocol, and a docs/wsl/wslc-state-aware.md update.

Additional context

Activity

  1. added
    Needs-TriageHas not yet been reviewed, classified, and routed.
    Issue-TaskEngineering, maintenance, or operational work that is not a bug or feature request.
    on Aug 13, 2026
  2. added
    Needs-TriageHas not yet been reviewed, classified, and routed.
    and removed
    Needs-TriageHas not yet been reviewed, classified, and routed.
    on Aug 13, 2026
  3. added
    Area-SchemaVersioned configuration contracts, JSON schemas, compatibility, or generated wire types.
    Backend-IsolationSessionWindows IsolationSession backend behavior or integration.
    Backend-VMNew VM isolation backends in the backlog
    Backend-WSLCWindows WSL Container backend behavior or integration.
    OS-WindowsApplies specifically to Windows hosts or Windows behavior.
    on Aug 13, 2026
  4. 6 remaining items

  5. github-actions commented on Aug 26, 2026

    @github-actions

    Triage summary

    This issue was already partially triaged. The existing labels and assignments look appropriate:

    Labels applied: OS-Windows, Container-WSLC, Area-SDK-Api, Area-Executor-Schema

    Assigned owners:

    • Soham Das (@SohamDas2021) — WSLc / daemon protocol (defects 1–4)
    • @adpa-ms — IsolationSession null-pipe pattern (defect 2 extension)
    • @huzaifa-d — Windows Sandbox null-pipe pattern (defect 2 extension)

    No additional labels or owners were needed based on this analysis. Needs-Triage is already removed.

    Maintainers: Comment /investigate to check whether this issue or bug is valid against the most current code. Copilot will also produce a report with the changes that will be needed. For a small, unambiguous documentation or test fix, it may also create one draft PR.

    Generated by 🏷️ Issue Triage for #843 · sonnet46 · 22.6 AIC · ⌖ 3.95 AIC · ⊞ 7.7K · ◷

  6. SohamDas2021 commented on Sep 15, 2026

    @SohamDas2021
    ContributorAuthor

    Filed #1167 to track WSLC specific work.

  7. adpa-ms commented on Sep 23, 2026

    @adpa-ms
    Contributor

    Checked the IsolationSession side of this against main @ 816cc3e. Nothing remains for IsolationSession.

  8. added
    Area-Rust-SDKPublic Rust SDK APIs and integration with the MXC execution engine.
    Backend-IsolationSessionWindows IsolationSession backend behavior or integration.
    Backend-WindowsSandboxWindows Sandbox VM backend behavior or integration.
    Release v1.0Issues tracked for the version 1.0 release
    and removed
    Area-SchemaVersioned configuration contracts, JSON schemas, compatibility, or generated wire types.
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Area-Rust-SDKPublic Rust SDK APIs and integration with the MXC execution engine.Backend-IsolationSessionWindows IsolationSession backend behavior or integration.Backend-WSLCWindows WSL Container backend behavior or integration.Backend-WindowsSandboxWindows Sandbox VM backend behavior or integration.Issue-TaskEngineering, maintenance, or operational work that is not a bug or feature request.OS-WindowsApplies specifically to Windows hosts or Windows behavior.Priority2High-impact issue affecting key functionality; prioritize for the next appropriate release.Release v1.0Issues tracked for the version 1.0 release

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions