You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Emit errors and warnings as structured ScriptResponse JSON - #1482
Executors now report failures and warnings as structured JSON instead of prose, so a caller can branch on a code rather than match backend-specific text.
Errors: each failure is one {"error":{"code","message"}} line on stderr. The exit code follows the code.
Warnings:ScriptResponse gains warnings (Option<Box<Vec<String>>>, which keeps the struct within the 128-byte result_large_err limit; a size test guards this). Executors write each warning as a {"warning":{"message"}} line on stderr.
Executor setup paths:wxc and lxc errors, including --setup-*, emit typed envelopes.
Code
Who can fix it
Exit
policy_validation
the request
1
backend_unavailable
the host
-1
backend_error
the runtime, during an accepted run
-1
malformed_request, unsupported_containment, other phase codes
the request
1
SDK compatibility: sites where a new code would break SDK v1 callers keep their v1 code. Each has a // SDK v2: comment naming its intended code. They are listed in docs/development/plans/sdk-v2-error-codes.md and tracked in #1595. The new codes apply everywhere else, for example Hyperlight, WSLC, Windows Sandbox and IsolationSession inherited stdio.
Out of scope:
warnings in the Rust, .NET and Node SDK result types.
Prose that backends still print alongside the envelope.
Entry-point failures printed a bare line and exited 1, which a caller could
not tell apart from a typed policy rejection, and warnings were written as
unstructured stderr text.
Classify every wxc-exec failure by whose problem it is, emit it as a typed
JSON envelope, and carry warnings on ScriptResponse.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backends reported failures inconsistently: some flattened everything to a
generic backend error, the Bubblewrap and LXC runners set no failure phase at
all, and WSLC rejections exited -1 while ProcessContainer rejections exited 1.
A caller could not tell a refused request from an unavailable host, and the
PowerShell suites had to match on prose banners to find out.
Classify each failure by whose problem it is and derive both the wire code and
the exit code from that:
the request is at fault Rejected policy_validation exit 1
the host is at fault BackendUnavailable backend_unavailable exit -1
the runtime faulted mid-run other phases backend_error exit -1
Every backend now sets a phase, and `ScriptResponse::failure_exit_code` is the
single place the exit code comes from.
Two consequences are worth calling out. A capability the host's Windows build
cannot represent -- `enumeratePaths`, `hostLoopback='allow'` -- now reaches the
wire as `policy_validation`, because dropping the field runs; it used to be
reported as an unavailable backend, which told the caller to give up on the
host. Conversely, a backend this build did not compile in now reports
`backend_unavailable` from the one-shot path too, matching what the state-aware
path already returned, because only a rebuilt executor can serve it.
Add an optional `warnings` array to `ScriptResponse` so the executors stop
interleaving advisory diagnostics with the error channel. It is boxed to keep
the struct under clippy's `result_large_err` threshold, and omitted from the
wire when a run produced none.
Fixes#612
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The P14b and P14c behavioural assertions gate on the `nativeCaptureAvailable`
probe, which answers whether the Learning Mode API is present. That is not the
same question as whether it is implemented: on a host whose exports resolve but
return E_NOTIMPL, the probe says the provider is usable, both runs then fail
before an output path exists, and P14c reports three failures on empty strings.
The runtime already classifies that outcome correctly -- `is_api_unavailable()`
maps it to `BackendUnavailable`, which reaches stderr as a typed
`backend_unavailable` envelope. Gate on what the run reported rather than on
the probe alone, via a shared `Test-BackendUnavailable`. Keying on the typed
code covers every reason a host cannot provide capture, not just the ones a
probe knows to ask about.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dispatch runs after the policy is accepted, so a failure there is the host
failing to build the tier the policy asked for, not a verdict on the request.
Classifying fallback and capture-provider failures as policy_validation made
wxc-exec log them as unsupported_field_for_backend and exit 1, which the
process-container suites read as a validation rejection of a config they
require to be accepted. Only a SID that could not be derived or ACEs that
could not be applied keep their own codes.
Update the harnesses that encoded the previous exit codes: WSLC and isolation
session rejections now exit 1, and the two process-container dispatch refusals
now exit -1. Match the Bubblewrap cwd advisory piecewise, since the advisory is
carried in a JSON warning envelope that escapes the quotes around the path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tier
Two different failures share DispatchError. A field the selected tier cannot
represent names that field and is documented as a rejection, so collapsing it
into backend_unavailable made hostLoopback=allow and enumeratePaths exit -1,
which the suites read as 'accepted and silently dropped' -- the one outcome
P8d and P8e exist to rule out. A tier that could not be built at all, or a
missing denial-capture provider, stays backend_unavailable: no tier existed to
judge the policy against.
Restore the enumeratePaths harness to exit 1 to match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Audit finalization raised its cleanup diagnostics as bare stderr prose, so a
caller had to scrape them while every other warning arrived as a JSON
envelope. They now travel on the response and are emitted with the rest.
The finalization failure arm hard-coded exit -1 regardless of how the backend
had already classified the run, which could pair a rejection envelope with an
exit status that contradicts it. Deriving the status from the failure phase
keeps the two in agreement, and the appended message reaches stderr through
the error envelope rather than a separate prose line.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Failure classification remains inconsistent across validation and streaming paths, and the Seatbelt availability check can reject valid default executions.
Elliot (theelliotm)
changed the title
Classify every backend failure by who can act on it
Emit errors and warnings as structured ScriptResponse JSON
Oct 13, 2026
Three surfaces reached the same condition by a different route and reported
it differently.
Shared request validation returned a runtime error, so an empty script was a
rejection through `validate` but a `backend_error` through `spawn`, which is
the path callers actually take. Both shared checks judge the request, so they
are rejections. `enumeratePaths` on a backend that cannot represent it now
also matches how the process-container dispatcher classifies the same field.
The streaming and PTY arms for a backend this build omits returned
`unsupported_containment`, while run-to-completion resolution called the same
build and host `backend_unavailable`. The code no longer depends on which
operation the caller chose. Arms for a backend belonging to another operating
system are untouched.
Seatbelt probed for Apple's sandbox CLI on every launch, but only the `Open`
method shells out to it; `Exec`, the default, applies the profile through
`sandbox_init`. A host without the CLI no longer loses the launch method that
does not need it.
Documents the stderr warning envelope alongside the response array, since the
two shapes differ and only the array was described, and records why a missing
experimental opt-in is a host-side code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drain retained telemetry warnings before early exits
src/tools/wxc/src/main.rs:1424
This early exit occurs after telemetry::init may have retained a warning (for example, ETW registration failure), but it bypasses the only one-shot warning drain at line 1660. The error envelope is emitted while that warning is silently lost from stderr, despite warnings being documented as emitted on failures too. Drain structured warnings before exiting; the same is needed at the other post-telemetry early exits.
This issue also appears in the following locations of the same file:
I reviewed the 43-file diff from base 8188df6 to head 9b7a541 and confirmed that its normalized contents match gh pr diff 1482. The inline comments below are anchored only on added lines; unchanged supporting files were byte-compared with the base. The strongest gap is that Unix executable tests do not assert the new JSON-code/exit contract. This is a COMMENT review, not a request for changes based on claim-only findings. No local build, platform-dependent integration suite, or end-to-end test was run as part of filing this review.
Findings on existing open threads
#3, live Bubblewrap DNS failures mislabeled as policy refusal: continued in discussion_r4277475899.
#6, state-aware error/status mismatch with the PR's blanket claim: continued in discussion_r4277476083 as claim_mismatch, not an introduced runtime regression.
#13, warning JSON test can miss a malformed line: continued in discussion_r4277475994.
#20, ProcessContainer DACL warnings plus other unstructured wxc-exec advisories despite “Resolves #612”: continued in discussion_r4277476034 as claim_mismatch.
Claim-only and unchanged supporting locations
#15 (claim_mismatch, Medium): new docs/development/architecture/versioning.md:718-719 promises wrong-OS unsupported_containment, but the byte-identical state-aware WSLC/IsolationSession gates still return backend_unavailable. The inline comment targets the new claim, not the old gates.
#16-19 (newly_exposed_by_change, Medium): the changed IsolationSession policy mapping, one-shot parser, Hyperlight policy phase, and Windows WSLC feature-off arm leave docs/development/architecture/backends/isolation-session/oneshot.md, the one-shot paragraph of docs/development/architecture/container-lifecycle.md, the earlier Hyperlight exit row, and docs/backends/wslc/wsl-container-getting-started.md stale. Each comment is anchored to the changed behavior rather than forcing an anchor into an unchanged paragraph.
#22 (claim_mismatch, Low): the PR's all-failures envelope wording includes dry-run failures, but the unchanged handle_dry_run_exit path in wxc, lxc, and mxc_darwin exits before emit_backend_error_envelope; dry-run callers still get a human summary rather than an error JSON line. Either emit the envelope on a failing dry run or limit the claim explicitly. This pre-existing early-exit path is not asserted as a new regression.
Verified pre-existing behavior, not attributed as a new regression
mxc_engine/state_aware.rs is byte-identical between base and head; it already returned backend_unavailable for certain missing-feature paths. The state-aware terminal branches already hard-coded exit 1. Finding #6 addresses only the PR's new universal promise.
The host DNS lookup in bubblewrap/common/proxy_network.rs is byte-identical. Finding #3 concerns the newrejected conversion of its errors at bwrap_runner.rs:472, not the resolver itself.
The older Seatbelt Exec-versus-Open rejection and the streaming feature-off classification have resolved review threads. New comments #11, #15, #19, and #24 address separate residual test, state-aware, and documentation gaps; they are not requests to reopen the fixed behavior.
The reason will be displayed to describe this comment to others. Learn more.
Medium (security) - workload stderr can impersonate MXC's JSON error.
Attribution:newly_exposed_by_change - the new backend_unavailable code is presented as a machine-readable host-failure signal on the same stderr channel the sandboxed program can write.
wxc-exec relays workload stderr before calling emit_backend_error_envelope; that emitter writes nothing for an ordinary nonzero workload exit because error_message is empty (script_runner.rs:104-120). A workload can print a plausible {"error":{"code":"backend_unavailable",...}} and exit nonzero. A CLI consumer parsing these lines cannot establish whether this diagnostic came from MXC or from contained code. This is conditional on a consumer trusting stderr; I did not find an in-repo SDK making fallback decisions from it.
Fix: Put executor-originated results on a host-owned channel the workload cannot write to. Until then, document these shared-stream lines as untrusted and do not recommend switching containment based solely on them.
The reason will be displayed to describe this comment to others. Learn more.
Medium, conditional CLI-consumer risk (still open). Attribution: newly_exposed_by_change for the newly meaningful backend_unavailable stderr signal. The new documentation correctly warns that workload stderr may contain forged envelope-shaped lines; that mitigates the claim but does not give a CLI consumer a way to authenticate MXC's JSON diagnostics. For a normal nonzero workload exit with empty error_message, emit_backend_error_envelope writes no authoritative result. I have not found an in-repo SDK making an unsafe backend selection from stderr, so this is not a demonstrated sandbox escape or a blocker on its own. If machine-readable CLI error codes are intended for decisions, provide a host-owned result channel; otherwise please track the channel work separately and keep the untrusted-stream limitation explicit.
The reason will be displayed to describe this comment to others. Learn more.
Medium (maintainability) - centralize phase-aware ScriptResponse construction.
Attribution:introduced_by_change - this PR adds the same response/phase assembly here and in lxc_runner.rs:47, appcontainer_runner.rs:95, seatbelt_runner.rs:1104, and isolation_session/common/sandbox.rs:91.
The implementations already differ in whether they copy the diagnostic into standard_err, which affects bare-prose duplication alongside the new JSON envelope. Maintaining one error contract through five near-identical constructors invites drift.
Fix: Put the phase-aware response construction in a small shared ScriptResponse constructor and use it at the backend call sites; preserve intentionally different stderr behavior explicitly.
The reason will be displayed to describe this comment to others. Learn more.
Changes requested
This follow-up changes my verdict from the COMMENT review 5534936733 on the same PR head (9b7a541). The 17 inline comments in that review and four subsequent replies already contain the detailed evidence; I am not opening duplicate threads.
Please address these change-attributable issues before merge:
High - Unix executor contract coverage (discussion_r4284388975, introduced_by_change): no Unix executor suite asserts both the structured error code and its exact process exit status for the new failure contract. Add a real refusal and unavailable-backend subprocess case, plus an envelope-serialization assertion.
Medium - Bubblewrap host DNS misclassified as request refusal (discussion_r4277475899, introduced_by_change): SandboxProxy::resolve can fail on host DNS timeout/thread failure, but the new rejected mapping emits policy_validation/exit 1. Keep static invalid-input errors as policy refusals and classify host lookup failures separately.
Medium - response warnings are missing for in-process callers (discussion_r4284389019, introduced_by_change): the new ScriptResponse.warnings field is not populated by mxc_engine::run even when the logger records advisories. Populate it at the shared run boundary or narrow the published response-level contract.
The state-aware exit mismatch in discussion_r4277476083 predates this PR and is a claim_mismatch, not a merge condition by itself. Other Medium/Low comments remain actionable but are not the basis for this verdict. The PR base and head are unchanged since the earlier review; no local build or host-dependent test suite was run for this follow-up.
Seatbelt profile preparation moved to profile_preparation.rs on main; its path and working-directory errors now classify request faults as policy_validation and host faults as backend_error.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1c677fc2-77c3-43b0-a06f-da11d11447c8
Share the path-vs-host I/O classification between bwrap and Seatbelt, and gate the Windows-only hyperlight Ambiguous variant.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1c677fc2-77c3-43b0-a06f-da11d11447c8
Restore SDK v1 codes at sites where the new classification would change
published SDK behavior, with a `// SDK v2:` comment naming the intended
code. Document the deferred work in
docs/development/plans/sdk-v2-error-codes.md (tracked in #1595).
Also address review feedback: Hyperlight permission-denied is a policy
refusal, wxc/lxc --setup-* errors emit typed envelopes, and a Bubblewrap
spawn failure is covered by a test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d3b6643f-9e96-44f0-9860-8c7a1796421a
The timeout scenario now requires exit -1 and a backend_error envelope
reporting the timeout. The not-compiled-backend test uses Hyperlight,
since MicroVM reports backend_unavailable on Windows.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d3b6643f-9e96-44f0-9860-8c7a1796421a
The reason will be displayed to describe this comment to others. Learn more.
Approving the current revision, c7bfbe5. I rechecked the previously requested Rust V1 error-code mappings, Hyperlight permission classification, structured setup errors, the Bubblewrap spawn-failure test, and the IsolationSession timeout assertion. All 29 PR checks pass. Focused local Windows tests passed for the ProcessContainer V1 mapping, the default-feature missing-backend mapping, and the Hyperlight permission classifier (3 tests); I did not run Linux/macOS paths locally.
This approval does not claim a clean scheduled host run: run 40594896918 on this head failed seven jobs (five WSLC image-pull failures, one WSLC stdin-output-loss test, and one MicroVM guest-startup failure). A later mainline run failed in the same WSLC/MicroVM categories, but those failures are not proven unrelated to this PR. The separate host-owned diagnostic channel is tracked in #1596; the remaining Bubblewrap launch/PTY coverage discussion is recorded on the PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📖 Description
Executors now report failures and warnings as structured JSON instead of prose, so a caller can branch on a code rather than match backend-specific text.
{"error":{"code","message"}}line on stderr. The exit code follows the code.ScriptResponsegainswarnings(Option<Box<Vec<String>>>, which keeps the struct within the 128-byteresult_large_errlimit; a size test guards this). Executors write each warning as a{"warning":{"message"}}line on stderr.wxcandlxcerrors, including--setup-*, emit typed envelopes.policy_validation1backend_unavailable-1backend_error-1malformed_request,unsupported_containment, other phase codes1SDK compatibility: sites where a new code would break SDK v1 callers keep their v1 code. Each has a
// SDK v2:comment naming its intended code. They are listed indocs/development/plans/sdk-v2-error-codes.mdand tracked in #1595. The new codes apply everywhere else, for example Hyperlight, WSLC, Windows Sandbox and IsolationSession inherited stdio.Out of scope:
warningsin the Rust, .NET and Node SDK result types.ScriptResponseare the authoritative result.🔗 References
Resolves #612
docs/development/architecture/versioning.md(## Error Contract); backend notes underdocs/backends/🔍 Validation
Local, Windows 11 ARM64, after merging
main:cargo fmt --all -- --checkcargo clippy -p mxc-sdk -p wxc -p lxc --all-targets --features "mxc-sdk/wslc mxc-sdk/isolation_session" -- -D warningscargo clippy -p mxc-sdk --all-targets --target x86_64-unknown-linux-gnu -- -D warningscargo clippy -p mxc-sdk --all-targets --target aarch64-apple-darwin -- -D warningscargo test -p mxc-sdk --lib --features "wslc isolation_session"main)cargo test -p wxcmain)Earlier scheduled validation run: 38377047465. It fixed the
[P8d]/[P8e]hostLoopbackfailures. Its debian13-bubblewrap failure is pre-existing onmain.Not run locally: host suites under
tests/scripts/(Bubblewrap, LXC, Seatbelt, WSLC, IsolationSession). They need a new scheduled run on this revision.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see pull request builds)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow