Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4a9c362
Give wxc-exec a typed error and warning contract
theelliotm Oct 12, 2026
4f6c81d
Classify every backend failure by which side can act on it
theelliotm Oct 12, 2026
1067ca9
Skip capture assertions where the host reports backend_unavailable
theelliotm Oct 12, 2026
f9d03b9
Report a tier the host could not build as backend_unavailable
theelliotm Oct 13, 2026
ad5b809
Keep an unrepresentable policy field a rejection, not an unavailable …
theelliotm Oct 13, 2026
0a3b502
Merge remote-tracking branch 'origin/main' into user/emichlin/cross-b…
theelliotm Oct 13, 2026
1e1923d
Route audit cleanup diagnostics through the warning contract
theelliotm Oct 13, 2026
a39b1d0
Apply one classification to the paths that still disagreed
theelliotm Oct 13, 2026
9b7a541
Merge remote-tracking branch 'origin/main' into user/emichlin/cross-b…
theelliotm Oct 14, 2026
fcb5c5a
Remove Seatbelt sandbox-exec availability check
theelliotm Oct 15, 2026
04d2170
Changes in response to Gudge's feedback
theelliotm Oct 15, 2026
387df58
Centralize phase-aware ScriptResponse construction
theelliotm Oct 15, 2026
5267817
Fix engine warning tests failing on Windows and macOS
theelliotm Oct 16, 2026
bc01847
Address Copilot review: timeout phase, warning line start, lifecycle …
theelliotm Oct 16, 2026
a132298
Merge remote-tracking branch 'origin/main' into user/emichlin/cross-b…
theelliotm Oct 16, 2026
c1c3b29
fixes: teardown outcome shadowing execoutcome, ScriptResponse default…
theelliotm Oct 16, 2026
ab78432
Classify Seatbelt profile host errors and emit dry-run error envelopes
theelliotm Oct 16, 2026
2ed5f6f
Keep IsolationSession timeouts typed and envelopes on fresh lines
theelliotm Oct 16, 2026
cce533e
Merge origin/main
theelliotm Oct 16, 2026
3afba24
Report relayed lifecycle exec timeouts as backend errors
theelliotm Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/backends/bwrap/bubblewrap-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,21 @@ tests/scripts/run_bwrap_all_tests.sh

Test configs are in `tests/configs/bubblewrap_*.json`.

## Troubleshooting

Failures carry an error code: `policy_validation` (exit 1) when the request
asks for something the backend refuses, `backend_unavailable` (exit -1) when
the host cannot provide the backend at all, and `backend_error` (exit -1) for
setup and runtime faults.

| Message | Meaning |
|---|---|
| `Bubblewrap (bwrap) is not installed or not on PATH` / version too old | The host has no usable Bubblewrap (`backend_unavailable`); install `bubblewrap` 0.5+ |
| `requires 'slirp4netns' on PATH` / `requires unprivileged user and network namespaces` | The host cannot provide the sandbox network (`backend_unavailable`) |
| `script_code is empty` / `must not carry credentials` / an egress or proxy refusal | The request is at fault (`policy_validation`); change it and retry |
| `failed to spawn bwrap` / `failed to allocate PTY` / proxy namespace start failure | The request was accepted but the launch faulted (`backend_error`) |
| `failed to wrap stdio pipes` | The sandbox started and then faulted (`backend_error`) |

## Limitations

- **Linux only** β€” Bubblewrap requires Linux kernel namespaces
Expand Down
13 changes: 10 additions & 3 deletions docs/backends/hyperlight/hyperlight-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ to the snapshot between calls.

| Aspect | Behaviour |
|---|---|
| Exit code | The script's: `sys.exit(N)`, `process.exit(N)` or `exit N` gives `N`, an uncaught exception gives `1`, a runner error gives `-1` with the reason in `error_message` |
| Exit code | The script's: `sys.exit(N)`, `process.exit(N)` or `exit N` gives `N`, an uncaught exception gives `1`. A refused request gives `1` and a host or runtime fault gives `-1`, with the reason in `error_message` |
| Timeout | `process.timeout` bounds the run; a guest that overruns is interrupted and the run reports `execution timed out` |
| stdout / stderr | Inherited by the guest; `ScriptResponse.standard_out` stays empty, so capture at the process level |

Expand Down Expand Up @@ -155,11 +155,18 @@ contain whitespace, `:` or brackets, and two mounts may not share one.

## Troubleshooting

Failures carry an error code: `policy_validation` (exit 1) when the request
asks for something the backend refuses, `backend_unavailable` (exit -1) when
the host cannot provide the backend at all, and `backend_error` (exit -1) for
setup and runtime faults.

| Message | Meaning |
|---|---|
| `no hyperlight image found for the <runtime> runtime` | No home holds that runtime; run `--setup-hyperlight=<runtime>`, or set `MXC_HYPERLIGHT_HOME` |
| `hyperlight policy error: …` | The request is at fault (`policy_validation`); change it and retry |
| `no hyperlight image found for the <runtime> runtime` | No home holds that runtime (`backend_unavailable`); run `--setup-hyperlight=<runtime>`, or set `MXC_HYPERLIGHT_HOME` |
| `holds a rootfs from another hyperlight-unikraft release` | The directory predates the crate the binary was built with; `--setup-hyperlight=<runtime>` rebuilds it |
| `Hyperlight requires KVM` / `Hyperlight requires Windows Hypervisor Platform` | No hypervisor for this process (error code `backend_unavailable`): on Linux `/dev/kvm` is missing or not readable and writable, on Windows WHP is not enabled |
| `Hyperlight requires KVM` / `Hyperlight requires Windows Hypervisor Platform` | No hypervisor for this process (`backend_unavailable`): on Linux `/dev/kvm` is missing or not readable and writable, on Windows WHP is not enabled |
| `hyperlight setup error: …` / `hyperlight runtime error: …` | The run was accepted but faulted (`backend_error`) |
| `execution timed out after N s` | The script overran `process.timeout` |

Run with `--debug` to see the home chosen, restore and call timings, and
Expand Down
15 changes: 15 additions & 0 deletions docs/backends/lxc/lxc-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,21 @@ sudo firewall-cmd --get-zone-of-interface=lxcbr0

The zone query should answer the zone you assigned.

## Troubleshooting

Failures carry an error code: `policy_validation` (exit 1) when the request
asks for something the backend refuses, `backend_unavailable` (exit -1) when
the host cannot provide the backend at all, and `backend_error` (exit -1) for
setup and runtime faults.

| Message | Meaning |
|---|---|
| `the LXC runtime is not available on this host` | `lxc-ls --version` did not run (`backend_unavailable`); install the LXC tools |
| `sandboxes can only be launched on Linux` | Wrong OS for the backend (`backend_unavailable`) |
| `distribution and release are required` / a proxy, egress or inherited-stdio refusal | The request is at fault (`policy_validation`); change it and retry |
| `Failed to create container` / `Failed to start container` / `container '…' is already in use` | The request was accepted but the launch faulted (`backend_error`) |
| `did not receive an IPv4 address` / `Failed to discover the container init PID` | The container started and then faulted before the script ran (`backend_error`) |

## Limitations

- **Default-deny is not a containment boundary against the workload.** Both the
Expand Down
4 changes: 3 additions & 1 deletion docs/backends/seatbelt/seatbelt-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,9 @@ once you can see the rules that were emitted.
### What gets rejected

MXC refuses any config it cannot faithfully enforce, rather than quietly
approximating it. This is the complete list.
approximating it. This is the complete list. A refusal carries the
`policy_validation` error code and exit code `1`: nothing ran, and only a
changed config can succeed.

One documented exception: `ingress.hostLoopback: "deny"` under
`ingress.default: "allow"` is accepted with the container-to-host half enforced
Expand Down
5 changes: 3 additions & 2 deletions docs/backends/wslc/wsl-container-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,9 @@ request is validated by the same native engine as executor requests.

Notes and limits:

- **Windows only.** Selecting `Containment::Wslc` on Linux/macOS, or without the
`wslc` feature, fails with `ErrorCode::UnsupportedContainment`.
- **Windows only.** Selecting `Containment::Wslc` on Linux/macOS fails with
`ErrorCode::UnsupportedContainment`; on Windows without the `wslc` feature it
fails with `ErrorCode::BackendUnavailable`.
- **No stdin.** The WSLC SDK exposes no process-input API, so
`Sandbox::take_stdin()` returns `None` for a WSL container.
- **No host pid.** The process runs inside the WSL VM, so `Sandbox::id()` is `0`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,10 @@ the rationale for each disposition, and the error mapping live in
| `isolationSession` / one-shot `appId` | rejected as `malformed_request` β€” IsolationSession one-shot configuration uses only the stable top-level policy |
| `processContainer` / `lxc` / `seatbelt` / another backend's section | rejected β€” only the section matching `containment` is accepted |

Refusals surface as a non-zero exit with the reason on stderr. One-shot has no
typed policy error code: the envelope carries `error.code = "backend_error"` with
the reason in the message. On the state-aware surface, structurally
representable backend policy failures emit `policy_validation`; fields excluded
by an exact phase root fail earlier as `malformed_request`.
Refusals surface as exit `1` with a `policy_validation` error envelope on
stderr naming the reason. An unreachable IsolationSession service reports
`backend_unavailable`. On the state-aware surface, fields excluded by an exact
phase root fail earlier as `malformed_request`.

**Why every supplied `ui` is refused.** The `ui` section states intent about the
contained code's relationship to the *user's* environment, and was modelled on a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,13 @@ as `malformed_request`. Requests that pass the exact structural contract but
violate a backend semantic invariant surface as `policy_validation`; a
structurally valid but oversized `appId` is one such case.

On the **one-shot** surface the backend's typed policy variant is discarded
(`ScriptResponse::error`) and the envelope carries `error.code =
"backend_error"` with the reason in the message. A supplied `network.proxy` is
also structurally refused as `malformed_request`.
On the **one-shot** surface the backend's typed policy variant becomes
`ScriptResponse::rejected`, so the envelope carries `error.code =
"policy_validation"` and exits 1, matching the state-aware mapping. A supplied
`network.proxy` is also structurally refused as `malformed_request`. An
activation failure of the runtime API becomes `ScriptResponse::unavailable`
(`backend_unavailable`, exit -1); every other lifecycle failure stays
`backend_error`.

## Mode-specific fields

Expand Down Expand Up @@ -502,13 +505,11 @@ State-aware exec (and other phases) use OS-level cancellation in v1:
- On the `wxc-exec` route, cancellation is process termination.
- The agent process's pipes EOF, the relay threads exit.
- The OS-side service's per-process timer (set from
`process.timeout`) reaps the agent if the runner does not. On the
streaming path that timer is armed with a **margin** past the caller's
deadline, so it acts purely as a watchdog: the service kills with an
ordinary exit code (the host suite pins it as exit code 1), so if it
fired first a genuine timeout would be indistinguishable from a normal
exit and could not be reported as one. The run-to-completion path arms
it unchanged β€” it has no timeout channel to report through.
`process.timeout`) reaps the agent if the runner does not. That timer is
armed with a **margin** past the caller's deadline, so it acts purely as
a watchdog: the service kills with an ordinary exit code, so if it fired
first a genuine timeout would be indistinguishable from a normal exit
and could not be reported as one.
- The runner's existing 3-tier shutdown (`CloseStandardInput` β†’
`SendCtrlClose` β†’ `Terminate`) handles the timeout case from inside the
agent process before returning.
Expand Down Expand Up @@ -556,7 +557,8 @@ An exited process is never routed through the shutdown ladder, which reads only
and so cannot tell a `259` exit from a live process. The adapter maps
`TimedOut` onto `ErrorKind::TimedOut`, which is what
`mxc_sdk::v1::MxcProcess::wait` reads as `WaitResult::TimedOut`. That outcome
is reachable only under `Piped`; the `Relayed` arm reports `Exited`.
is reachable only under `Piped`; the `Relayed` arm reports a timeout as a
`backend_error` instead.

Teardown is bounded only insofar as the kill is: the streaming adapter's
`Drop` joins the waiter when the kill was accepted, and abandons the
Expand Down
11 changes: 7 additions & 4 deletions docs/development/architecture/container-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,9 +574,9 @@ diagnostic. The rejection is recorded through the same `ConfigRejected` audit pa
Diagnostics are never duplicated to stdout, and lifecycle stderr is not gated on
`--debug`.

Without `--operation`, `wxc-exec` accepts only one-shot requests. Input-source and
pre-parse failures on that path retain the legacy primary diagnostic on stderr and do
not emit a lifecycle envelope. Raw SDK and FFI lifecycle calls do not use this CLI
Without `--operation`, `wxc-exec` accepts only create-and-run requests. Input-source
and pre-parse failures on that path emit a typed `{error}` envelope on stderr rather
than the lifecycle envelope on stdout. Raw SDK and FFI lifecycle calls do not use this CLI
stream protocol; they return their status and error data through their binding result
surfaces.

Expand Down Expand Up @@ -629,7 +629,10 @@ because a script's stderr may not end in a newline.
`code` and `message` are always present. `code` is the machine-readable category a
consumer branches on; `message` is the human-readable description, and for a failure
raised by an underlying platform API it is that API's own message, passed through
verbatim rather than concatenated with the other fields.
verbatim rather than concatenated with the other fields. The executor's own exit
code follows `code`: a refused request exits `1`, and an unavailable host or a
runtime fault exits `-1`. See the error contract in
Comment thread
theelliotm marked this conversation as resolved.
[versioning.md](versioning.md#error-contract).

The three optional named fields carry structured failure detail. `operation` and
`nativeCode` describe an underlying platform call; `remediation` describes the failure:
Expand Down
41 changes: 41 additions & 0 deletions docs/development/architecture/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,47 @@ Negotiation failures are **typed and actionable** β€” never a silent fallback:
unsupported field. Security policy is deterministic β€” no relaxation, no fuzzy
fallback.

Every executor failure is classified by **which side can act on it**, and that
classification drives both the wire code and the process exit code:

| Side at fault | Failure phase | Wire code | Exit code |
|---|---|---|---|
| The request β€” a different request succeeds | `Rejected` | `policy_validation` | `1` |
| The host β€” no request change helps | `BackendUnavailable` | `backend_unavailable` | `-1` |
| The runtime, during an accepted run | all other phases | `backend_error` | `-1` |
Comment thread
Copilot marked this conversation as resolved.

A backend this build did not compile in reports `backend_unavailable`, since
only a rebuilt executor can serve it. On the create-and-run path, a backend
belonging to another operating system reports `unsupported_containment`, since
naming a supported backend succeeds; the container lifecycle operations report
such a backend as `backend_unavailable`. Within a backend, a field the selected
tier cannot represent (for example `filesystem.enumeratePaths` or
`network.ingress.hostLoopback` on an AppContainer fallback), or a fallback the
request forbids (`fallback.allowDaclMutation: false` when only the DACL tier is
usable), is a `policy_validation` rejection naming that field, never a silent
drop; but a tier that could not be built at all, or an absent denial-capture
provider, is `backend_unavailable`, because the policy was accepted and never
judged. An experimental backend named without the experimental opt-in is also
`backend_unavailable`: the opt-in is an executor invocation mode rather than a
policy field, so no change to the authored request reaches it. Earlier
phase-specific codes (`malformed_request`, `unsupported_containment`,
`unsupported_phase`, `malformed_id`) are unchanged and also exit `1`.

Envelope lines share stderr with the workload's own output, so a workload can
print text that looks like an envelope. Treat stderr envelopes as diagnostics,
not as an authenticated result; the exit code and a held `ScriptResponse` are
authoritative.

`ScriptResponse` carries an optional `warnings` array of diagnostic strings.
Warnings are advisory and never change the exit code; they are omitted from the
wire entirely when a run produced none. The serialized response and the
executor's stderr stream are two distinct shapes of the same warnings: on the
response they are a `warnings: string[]` field, while an executor writes each
one to stderr as its own line-delimited JSON object,
`{"warning":{"message":"..."}}`, matching the error envelope
`{"error":{"code":"...","message":"..."}}`. A caller reading stderr must parse
the per-line objects; the array exists only on a response it holds directly.

The MXC ↔ OS contract therefore reports: which feature failed, whether it was a
version mismatch or a runtime/capability unavailability (e.g. Hyper-V off), and
what the user should do (upgrade OS, enable feature, change the config).
Expand Down
Loading
Loading