feat(mxc-sdk): add the importable in-process sandbox library crate#556
Merged
Conversation
…d / SandboxProcess / Runner)
Introduce the shared, no-pty execution surface that the containment backends
and a future in-process library build on. Purely additive: no existing code
path uses these yet, so behavior is unchanged.
- `SandboxProcess` — a handle to a running sandboxed child: `take_stdin` /
`take_stdout` / `take_stderr`, `try_wait`, `id`, `kill` (process-tree), and
`wait` (drains any untaken stdio, honors `scriptTimeout`), plus stdout/stderr
closers for abandoning a backgrounded-descendant-held read without a kill.
- `SandboxBackend` — `validate` + `spawn(request, logger, StdioMode) ->
SandboxProcess` + a `diagnose_exit` hook; `StdioMode::{Pipes, Inherit}`.
- `Runner<B>` — the generic adapter bridging any `SandboxBackend` to the
run-to-completion `ScriptRunner` (spawn `Inherit`, then `wait`).
- `StreamCloser`, `group_kill` (Unix leader-first SIGKILL of the child's group),
and `wait_with_timeout` (adaptive 1ms->50ms backoff poll).
- `interruptible_reader` (Unix self-pipe + `poll`) and the Windows pipe helpers
in `process_util` (`InterruptiblePipeReader` / `PipeReadCanceller` /
`create_std_pipes`) for out-of-band-cancelable streaming reads.
- `FailurePhase::Timeout` so a timeout is distinguishable from other failures.
The library backends and executor binaries are migrated onto this surface in a
follow-up PR, and the importable `mxc-sdk` crate is built on top of it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
…ute binaries via Runner Migrate the three in-process backends — Seatbelt (macOS), Bubblewrap (Linux), and Windows ProcessContainer (AppContainer + BaseContainer) — onto the SandboxBackend / SandboxProcess interfaces added in the previous PR, and route the executor binaries (wxc-exec, lxc-exec, mxc-exec-mac) through the generic Runner<B> adapter. The old per-backend run-to-completion logic is removed; each backend now exposes only spawn(), and Runner provides the single ScriptRunner the binaries dispatch on (spawn StdioMode::Inherit, then wait). Each backend gains a streaming handle with whole-process-tree termination (Unix process-group SIGKILL; Windows job-object terminate) and a uniform io::ErrorKind::TimedOut on scriptTimeout. Intentional behavior changes for existing binaries (call-outs for review): - Seatbelt now always env_clear()s the child (previously only when process.env was non-empty), aligning the binary with the SDK's documented "host env is not inherited" contract. - Seatbelt resolves an empty process.cwd to a policy read-write path (or "/") instead of the launcher's cwd. - Seatbelt/Bubblewrap inherit the executor's own stdio (StdioMode::Inherit) — Seatbelt no longer allocates a private pty, and Bubblewrap no longer forces stdin to /dev/null or post-exit-captures stdout/stderr (it streams live). - BaseContainer now places the child in a UiJobObject for tree-kill (it had none before); the child is created suspended, assigned to the job, then resumed so no descendant can escape the kill window. - kill() is a no-op once the child has been reaped, so it never signals a recycled pid/process-group. The macOS Seatbelt characterization suite is updated to assert the new env/cwd/ streaming/timeout behavior; the LXC and Seatbelt backend docs are updated to match. The default LXC path keeps its native pty and is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Add `mxc-sdk` (lib `mxc_sdk`), a Rust library for starting MXC sandboxes in-process without a pty, built on the SandboxBackend interfaces and the unified backends from the previous PRs. Callers stream a sandboxed process without shelling out to the executor binaries or depending on the TypeScript SDK. Public surface (crate-owned types — `wxc_common` stays an implementation detail): - `build_request(&SandboxPolicy)` -> `SandboxRequest`, then `spawn_sandbox` -> `Sandbox`: a handle for bidirectional stdio (`take_stdin`/`take_stdout`/ `take_stderr`), `try_wait`, `id`, `kill` (process-tree), and `wait` -> `WaitOutcome` (`Exited(i32)` | `TimedOut`), plus stdout/stderr closers. - `Error` / `ErrorCode` mirror the wire-format error one-for-one. - `mxc_sdk::policy` ports the SDK's config building (`createConfigFromPolicy` plus `available_tools_policy` / `user_profile_policy` / `temporary_files_policy`); `platform_support` ports `getPlatformSupport`. Backends: Bubblewrap (Linux), Seatbelt (macOS), Windows ProcessContainer (AppContainer + BaseContainer). Other backends and LXC return `UnsupportedContainment` (LXC has no non-pty capture path); `dry_run` is rejected for streaming spawns. Adds `wxc_common::config_parser::load_request_from_value` so the crate maps a config it already holds as JSON without a base64 round-trip. The in-crate backend dispatch (`dispatch.rs`) and host probe (`platform.rs`) are marked provisional — a follow-up moves them into a shared `mxc` engine crate that both this library and the executor binaries call into. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new importable Rust library crate (mxc-sdk / mxc_sdk) to spawn MXC sandboxes in-process with a handle-based, no-pty streaming API. To support this, it introduces shared streaming execution interfaces in wxc_common, refactors supported backends to implement them, and routes existing executor binaries through a single run-to-completion adapter.
Changes:
- Introduce
wxc_common::sandbox_process(SandboxBackend/SandboxProcess/Runner) plus supporting timeout + interruptible-stdio plumbing. - Refactor Seatbelt (macOS), Bubblewrap (Linux), and ProcessContainer (Windows) backends to expose streaming handles and route CLI executors through
Runner. - Add the
mxc-sdkcrate (API, policy helpers, platform probing) with tests and documentation updates.
Show a summary per file
| File | Description |
|---|---|
| src/testing/wxc_e2e_tests/tests/e2e_seatbelt_characterization.rs | Updates macOS Seatbelt characterization assertions to match the unified runner behavior (env/cwd semantics). |
| src/core/wxc/src/main.rs | Switches Windows ProcessContainer CLI execution to use Runner adapter. |
| src/core/wxc_common/src/sandbox_process.rs | Adds new streaming execution traits, helpers, and Runner bridge. |
| src/core/wxc_common/src/process_util.rs | Adds Windows pipe reader/writer + interruptible read cancellation support. |
| src/core/wxc_common/src/models.rs | Adds FailurePhase::Timeout to distinguish timeout from normal nonzero exits. |
| src/core/wxc_common/src/lib.rs | Exposes new sandbox_process module and unix-only interruptible_reader. |
| src/core/wxc_common/src/interruptible_reader.rs | Adds unix interruptible pipe reader for cancelable blocking reads. |
| src/core/wxc_common/src/config_parser.rs | Adds load_request_from_value to map already-parsed JSON into ExecutionRequest. |
| src/core/wxc_common/Cargo.toml | Broadens libc dependency from linux-only to all unix targets. |
| src/core/mxc-sdk/tests/streaming.rs | Adds streaming API tests (stdio, kill, timeout); includes Windows integration test (ignored). |
| src/core/mxc-sdk/tests/sdk_helpers.rs | Adds tests for policy helper ports and platform support probing. |
| src/core/mxc-sdk/tests/sandbox.rs | Adds end-to-end library tests for env isolation, cwd defaulting, and timeout behavior. |
| src/core/mxc-sdk/src/sandbox.rs | Defines public Sandbox handle and WaitOutcome wrapper over internal streaming handle. |
| src/core/mxc-sdk/src/platform.rs | Implements host platform support detection for the Rust library. |
| src/core/mxc-sdk/src/lib.rs | Defines crate public surface and spawn_sandbox entrypoint. |
| src/core/mxc-sdk/src/error.rs | Adds crate-owned Error / ErrorCode facade over wxc_common errors. |
| src/core/mxc-sdk/src/dispatch.rs | Implements backend selection + spawn for supported streaming backends. |
| src/core/mxc-sdk/README.md | Documents crate usage, streaming model, and supported backends. |
| src/core/mxc-sdk/Cargo.toml | Adds new mxc-sdk crate and its per-platform backend dependencies. |
| src/core/mxc_darwin/src/main.rs | Routes macOS executor through Runner. |
| src/core/lxc/src/main.rs | Routes Bubblewrap executor path through Runner. |
| src/Cargo.toml | Registers new workspace member core/mxc-sdk and adds seatbelt_common workspace dep. |
| src/Cargo.lock | Adds mxc-sdk package; updates Seatbelt deps after removing mxc_pty usage. |
| src/backends/seatbelt/common/src/seatbelt_runner.rs | Refactors Seatbelt backend to SandboxBackend + streaming handle (pipes/inherit), plus cwd/env changes. |
| src/backends/seatbelt/common/src/profile_builder.rs | Updates TTY rules comments and exports expand_tilde for cwd resolution. |
| src/backends/seatbelt/common/Cargo.toml | Removes mxc_pty dependency from Seatbelt backend crate. |
| src/backends/bubblewrap/common/src/bwrap_runner.rs | Refactors Bubblewrap backend to SandboxBackend + streaming handle with proper teardown. |
| src/backends/appcontainer/common/src/probe.rs | Makes UiCapabilitySupport cloneable. |
| src/backends/appcontainer/common/src/network_manager.rs | Refactors firewall COM initialization to be per-call RAII, thread-safe for teardown on different threads. |
| src/backends/appcontainer/common/src/job_object.rs | Adds job-object termination helper for process-tree kill. |
| src/backends/appcontainer/common/src/dispatcher.rs | Wraps ProcessContainer runners in Runner for run-to-completion path. |
| src/backends/appcontainer/common/src/base_container_runner.rs | Refactors BaseContainer to SandboxBackend + streaming handle; adds job-based tree-kill and capture pipes. |
| docs/sandbox-policy/v1/policy.md | Updates allowedHosts/blockedHosts allowOutbound requirements across backends. |
| docs/macos-support/seatbelt-backend.md | Documents Seatbelt environment-clearing and cwd defaulting behavior. |
| docs/lxc-support/lxc-backend.md | Updates LXC env semantics wording after Seatbelt behavior change. |
| .github/copilot-instructions.md | Documents new mxc-sdk crate and the new streaming execution model (sandbox_process). |
Copilot's findings
- Files reviewed: 37/38 changed files
- Comments generated: 3
…fely
Taking both take_stdout() and take_stderr() and reading them sequentially
can deadlock an output-heavy child (one pipe fills while the reader is
blocked on the other). Add wait_with_output(): it consumes the handle,
drains stdout and stderr concurrently on separate threads, and returns
Output { outcome, stdout, stderr } -- the safe, convenient default,
mirroring std::process::Child::wait_with_output. Review CR-24.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
…ent/host The streaming dispatch in `dispatch.rs` only had direct tests for the `dry_run` and macOS `guiAccess` rejection branches. Add two more so the remaining guardrails are exercised in CI: - `streaming_rejects_unsupported_containment`: drives the internal model with `containment = Lxc` and asserts `UnsupportedContainment` plus the backend name in the message. - `host_support_ok_on_supported_platforms`: cfg-gated to Windows / Linux / macOS, guards against the `ensure_host_supported` cfg list dropping a supported platform. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
…set_env
set_env(Vec<String>) made the caller hand-format raw KEY=VALUE strings,
which diverged from the SDK's env channel -- injectEnvIntoConfig
(sdk/src/sandbox.ts) takes a structured { key: value } map and joins it to
the KEY=VALUE wire form internally.
Accept (key, value) pairs instead and do the formatting in the setter, so
the crate matches the SDK surface and callers can't forget the '='. The wire
representation (Vec<String> of KEY=VALUE) is unchanged, and iteration order
is preserved so a later duplicate key still wins downstream -- same as the
SDK. No eager validation is added: the SDK doesn't validate either, and
structured input already removes the malformed-entry foot-gun.
Adds a unit test asserting the pair-to-KEY=VALUE ordered mapping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
The self-pipe used to wake `InterruptibleReader`'s `poll` was created with `libc::pipe`, which does not set close-on-exec. Both wake fds would then leak into any process the thread later forks+execs (e.g. another sandbox child) and keep the wake pipe alive unexpectedly. Mark both ends `FD_CLOEXEC` after `pipe()`, mirroring the fixup `mxc_pty` already does for PTY fds. The data pipe is unaffected -- Rust already sets CLOEXEC on `Child` stdio. Addresses a Copilot review comment on #555. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
`group_kill` discarded both `kill(2)` results and always returned `Ok(())`, so `SandboxProcess::kill()` reported success even when it never signalled the process group. Route both signals through a `send_sigkill` helper that returns the error instead, treating only the "already gone" outcomes as success: `ESRCH`, and `EPERM` -- which on macOS a redundant kill of an exited-but-unreaped child's group reports in place of `ESRCH` (observed via the double-kill test). The caller guards with `try_wait()` first, so the pid/pgid can't be recycled and `EPERM` here can only be that benign race, never a real permission failure. Addresses a Copilot review comment on #556. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
… file `tests/streaming.rs` is `#![cfg(target_os = "macos")]`, so the `#[cfg(target_os = "windows")]` ProcessContainer streaming test it contained could never compile -- the intended Windows coverage was silently missing. Move that test into its own `tests/streaming_processcontainer.rs`, gated `#![cfg(target_os = "windows")]`, so it actually builds on Windows. Addresses a Copilot review comment on #556. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
# Conflicts: # src/core/wxc_common/src/interruptible_reader.rs # src/core/wxc_common/src/sandbox_process.rs
MGudgin
pushed a commit
that referenced
this pull request
Jun 24, 2026
…ute binaries via Runner (#555) * feat(wxc_common): add the sandbox execution interfaces (SandboxBackend / SandboxProcess / Runner) Introduce the shared, no-pty execution surface that the containment backends and a future in-process library build on. Purely additive: no existing code path uses these yet, so behavior is unchanged. - `SandboxProcess` — a handle to a running sandboxed child: `take_stdin` / `take_stdout` / `take_stderr`, `try_wait`, `id`, `kill` (process-tree), and `wait` (drains any untaken stdio, honors `scriptTimeout`), plus stdout/stderr closers for abandoning a backgrounded-descendant-held read without a kill. - `SandboxBackend` — `validate` + `spawn(request, logger, StdioMode) -> SandboxProcess` + a `diagnose_exit` hook; `StdioMode::{Pipes, Inherit}`. - `Runner<B>` — the generic adapter bridging any `SandboxBackend` to the run-to-completion `ScriptRunner` (spawn `Inherit`, then `wait`). - `StreamCloser`, `group_kill` (Unix leader-first SIGKILL of the child's group), and `wait_with_timeout` (adaptive 1ms->50ms backoff poll). - `interruptible_reader` (Unix self-pipe + `poll`) and the Windows pipe helpers in `process_util` (`InterruptiblePipeReader` / `PipeReadCanceller` / `create_std_pipes`) for out-of-band-cancelable streaming reads. - `FailurePhase::Timeout` so a timeout is distinguishable from other failures. The library backends and executor binaries are migrated onto this surface in a follow-up PR, and the importable `mxc-sdk` crate is built on top of it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * refactor(backends): unify the library backends on SandboxBackend + route binaries via Runner Migrate the three in-process backends — Seatbelt (macOS), Bubblewrap (Linux), and Windows ProcessContainer (AppContainer + BaseContainer) — onto the SandboxBackend / SandboxProcess interfaces added in the previous PR, and route the executor binaries (wxc-exec, lxc-exec, mxc-exec-mac) through the generic Runner<B> adapter. The old per-backend run-to-completion logic is removed; each backend now exposes only spawn(), and Runner provides the single ScriptRunner the binaries dispatch on (spawn StdioMode::Inherit, then wait). Each backend gains a streaming handle with whole-process-tree termination (Unix process-group SIGKILL; Windows job-object terminate) and a uniform io::ErrorKind::TimedOut on scriptTimeout. Intentional behavior changes for existing binaries (call-outs for review): - Seatbelt now always env_clear()s the child (previously only when process.env was non-empty), aligning the binary with the SDK's documented "host env is not inherited" contract. - Seatbelt resolves an empty process.cwd to a policy read-write path (or "/") instead of the launcher's cwd. - Seatbelt/Bubblewrap inherit the executor's own stdio (StdioMode::Inherit) — Seatbelt no longer allocates a private pty, and Bubblewrap no longer forces stdin to /dev/null or post-exit-captures stdout/stderr (it streams live). - BaseContainer now places the child in a UiJobObject for tree-kill (it had none before); the child is created suspended, assigned to the job, then resumed so no descendant can escape the kill window. - kill() is a no-op once the child has been reaped, so it never signals a recycled pid/process-group. The macOS Seatbelt characterization suite is updated to assert the new env/cwd/ streaming/timeout behavior; the LXC and Seatbelt backend docs are updated to match. The default LXC path keeps its native pty and is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix(wxc_common): set FD_CLOEXEC on the interruptible-reader wake pipe The self-pipe used to wake `InterruptibleReader`'s `poll` was created with `libc::pipe`, which does not set close-on-exec. Both wake fds would then leak into any process the thread later forks+execs (e.g. another sandbox child) and keep the wake pipe alive unexpectedly. Mark both ends `FD_CLOEXEC` after `pipe()`, mirroring the fixup `mxc_pty` already does for PTY fds. The data pipe is unaffected -- Rust already sets CLOEXEC on `Child` stdio. Addresses a Copilot review comment on #555. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix(wxc_common): propagate non-benign group_kill signal errors `group_kill` discarded both `kill(2)` results and always returned `Ok(())`, so `SandboxProcess::kill()` reported success even when it never signalled the process group. Route both signals through a `send_sigkill` helper that returns the error instead, treating only the "already gone" outcomes as success: `ESRCH`, and `EPERM` -- which on macOS a redundant kill of an exited-but-unreaped child's group reports in place of `ESRCH` (observed via the double-kill test). The caller guards with `try_wait()` first, so the pid/pgid can't be recycled and `EPERM` here can only be that benign race, never a real permission failure. Addresses a Copilot review comment on #556. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MGudgin
approved these changes
Jun 24, 2026
MGudgin
pushed a commit
that referenced
this pull request
Jun 24, 2026
This change adapts the Phase 2B strict-wire-model parser to two crates that landed on main after 2B was branched, surfaced when rebasing 2B onto current main: the new in-process `mxc-sdk` library crate (#556) and the `load_request_from_value` entrypoint (#554/#556). Details: - config_parser.rs: port `load_request_from_value` (added on main against the deleted `RawConfig` API) to 2B's wire model — deserialize into `wire::MxcConfig` and call `convert_wire_config`, matching its sibling `load_request_with_options`. - core/mxc-sdk/src/policy.rs: stop emitting `processContainer.name` (the wire `ProcessContainer` is strict and has no `name` field; the id is already carried at top-level `containerId`). Suppress the now-unused `container_id` in the Windows block. - sdk/src/sandbox.ts: likewise stop emitting `processContainer.name`; drop the now-unused `containerId` parameter from `buildProcessBaseContainerConfig`. Tests: - cargo test --workspace -> all pass (the 6 mxc-sdk parse failures from the rejected `name` field are resolved). - cargo fmt --check + cargo clippy --workspace -D warnings -> clean. - schema codegen + corpus (169/169) + schema-version gates -> green. - cd sdk && npm test -> 183 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Generated-with: claude-opus-4.8
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Part 3 of 3 splitting #524. This PR adds
mxc-sdk(libmxc_sdk), an importable Rust library for starting MXC sandboxes in-process without a pty, built on theSandboxBackendinterfaces (#554) and the unified backends (#555). Callers stream a sandboxed process without shelling out to the executor binaries or depending on the@microsoft/mxc-sdkTypeScript module.Public surface — crate-owned types, so
wxc_commonstays an implementation detail callers don't depend on:build_request(&SandboxPolicy) -> SandboxRequest, thenspawn_sandbox -> Sandbox: a handle for bidirectional stdio (take_stdin/take_stdout/take_stderr),try_wait,id,kill(process-tree), andwait -> WaitOutcome(Exited(i32)|TimedOut;Errreserved for an actual OS/wait failure), plus stdout/stderr closers.Error/ErrorCodemirror the wire-format error one-for-one.mxc_sdk::policyports the SDK's config building (createConfigFromPolicyplusavailable_tools_policy/user_profile_policy/temporary_files_policy);platform_supportportsgetPlatformSupport.Backends: Bubblewrap (Linux), Seatbelt (macOS), Windows ProcessContainer (AppContainer + BaseContainer). Other backends and LXC return
UnsupportedContainment(LXC has no non-pty capture path);dry_runis rejected for streaming spawns.Adds
wxc_common::config_parser::load_request_from_valueso the crate maps a config it already holds as JSON without a base64 round-trip. The in-crate backend dispatch (dispatch.rs) and host probe (platform.rs) are marked provisional — a follow-up moves them into a sharedmxcengine crate that both this library and the executor binaries call into.🔗 References
mxc-sdkcommit here.🔍 Validation
cargo test -p mxc-sdk(macOS, toolchain 1.93): 44 pass (12 lib-unit,sandbox7,sdk_helpers10,streaming14, +1 doc-test); Windows ProcessContainer integration tests are#[ignore]d (need an elevated, host-prepped Windows host).cargo fmt --all -- --checkandcargo clippy --all-targets -- -D warningsclean on macOS, Windows, and Linux targets.cargo test -p wxc_common268 pass (validates theload_request_from_valueaddition).✅ Checklist
📋 Issue Type
Microsoft Reviewers: Open in CodeFlow