From 0c9d775cc72e5d6b17a1095da3fa6ad24e99a7cb Mon Sep 17 00:00:00 2001 From: Demitrius Nelon Date: Wed, 6 May 2026 14:08:32 -0700 Subject: [PATCH 1/5] Add ISV integration guidance to SDK README Add practical sections for external consumers: - Common Pitfalls (stderr merging, createConfigFromPolicy gotcha, elevation failures, timeout defaults) - Recommended Policy for Agentic Workloads - Error Handling patterns - Integration Checklist - Elevation requirement in Prerequisites Resolves #262 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/README.md | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/sdk/README.md b/sdk/README.md index 4fe446e5d..d13aca133 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -39,6 +39,7 @@ Then reference it from your project (e.g., via `npm link` or a relative path in **Requirements**: - **Windows**: Windows 11 build 26100+ with UBR ≥ 7965 (for builds 26100–26500) +- **Windows**: The host process must run **elevated (Administrator)** to create AppContainer-based sandboxes - **Linux**: LXC must be installed and available **Platform Support**: @@ -447,6 +448,120 @@ console.log('Output:', result.stdout); console.log('Exit code:', result.exitCode); ``` +## Common Pitfalls + +### `spawnSandboxAsync` merges stdout and stderr + +`spawnSandboxAsync` uses a PTY internally. **stderr is always empty** in the returned result — all output (including error output) arrives in `stdout`. If you need to distinguish stderr from stdout, you must handle it at the application level inside the sandbox (e.g., redirect stderr to a file, then read it back). + +```typescript +// result.stderr will always be '' — this is by design +const result = await spawnSandboxAsync('python script.py', policy); +console.log(result.stdout); // Contains both stdout AND stderr +console.log(result.stderr); // Always empty string +``` + +### `createConfigFromPolicy()` returns an empty command line + +If you use the advanced path (`createConfigFromPolicy()` → modify → `spawnSandboxFromConfig()`), the returned config has an **empty `process.commandLine`**. You must set it before spawning: + +```typescript +import { createConfigFromPolicy, spawnSandboxFromConfig } from '@microsoft/mxc-sdk'; + +const config = createConfigFromPolicy(policy); +config.process.commandLine = 'python -c "print(\'hello\')"'; // ← Required! +const pty = spawnSandboxFromConfig(config); +``` + +### Elevation failures on Windows + +If the host process is not elevated, `spawnSandbox` will throw. The error message may not immediately indicate a privilege issue. Always ensure your host process runs as Administrator during development and testing. + +### Timeouts default to no limit + +If you delegate work to a sandbox, set `timeoutMs` in your policy to prevent runaway processes: + +```typescript +const policy: SandboxPolicy = { + version: '0.4.0-alpha', + filesystem: { readonlyPaths: tools.readonlyPaths }, + timeoutMs: 30000, // 30 seconds +}; +``` + +## Recommended Policy for Agentic Workloads + +Agents that execute code on behalf of a user typically need scoped filesystem access and limited (or no) network. The `SandboxPolicy` model is **default-deny** — omitted fields are restrictive. + +```typescript +import { + SandboxPolicy, + getAvailableToolsPolicy, + getTemporaryFilesPolicy, +} from '@microsoft/mxc-sdk'; + +const tools = getAvailableToolsPolicy(process.env); +const temp = getTemporaryFilesPolicy(); + +const agentPolicy: SandboxPolicy = { + version: '0.4.0-alpha', + filesystem: { + readonlyPaths: [ + ...tools.readonlyPaths, + // Add paths to project files the agent needs to read + ], + readwritePaths: [ + ...temp.readwritePaths, + // Add a scoped output directory for agent artifacts + ], + }, + network: { + // Only enable if the agent needs external API access + allowOutbound: false, + // Or restrict to specific hosts: + // allowedHosts: ['api.example.com'], + }, + timeoutMs: 60000, // Prevent runaway execution +}; +``` + +**Principle:** Grant the sandbox only what the current task requires. If your agent performs multiple steps, consider spawning separate sandboxes with different policies for each step rather than one broad policy. + +## Error Handling + +```typescript +import { getPlatformSupport, spawnSandboxAsync } from '@microsoft/mxc-sdk'; + +// 1. Check platform before any sandbox operations +const support = getPlatformSupport(); +if (!support.isSupported) { + // Degrade gracefully — run without containment or inform the user + console.error(`MXC not available: ${support.reason}`); +} + +// 2. Wrap spawn calls in try/catch +try { + const result = await spawnSandboxAsync(command, policy); + if (result.exitCode !== 0) { + // Non-zero exit — check result.stdout for error output (stderr is merged) + handleFailure(result.stdout, result.exitCode); + } +} catch (err) { + // Spawn failures: privilege issues, invalid policy, missing binaries + handleSpawnError(err); +} +``` + +## Integration Checklist + +- [ ] `getPlatformSupport()` called at startup; graceful fallback if unsupported +- [ ] Host process runs elevated (Administrator) on Windows +- [ ] Policy uses default-deny; only required paths/network are enabled +- [ ] `timeoutMs` set to prevent unbounded execution +- [ ] `spawnSandboxAsync` stdout handled knowing stderr is merged +- [ ] Error handling covers both spawn failures and non-zero exit codes +- [ ] Tested on Windows 11 build 26100+ with UBR ≥ 7965 + ## TypeScript Support The package includes full TypeScript definitions. All public types are exported from the main entry point: From a33181a249e51eee035e124acc1b13db2732caee Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Tue, 12 May 2026 12:35:35 -0700 Subject: [PATCH 2/5] Add more changes for docs --- Readme.md | 2 +- .../UIPolicy_Schema.md | 0 docs/{ => lxc-support}/lxc-backend.md | 0 .../nanvix-integration-plan.md | 2 +- docs/{microvm.md => nanvix-microvm/nanvix.md} | 0 .../windows-sandbox-reference.md | 0 docs/{ => windows-sandbox}/windows-sandbox.md | 0 sdk/README.md | 724 ++++++------------ src/wxc_common/src/base_container_runner.rs | 267 +++++-- 9 files changed, 444 insertions(+), 551 deletions(-) rename docs/{ => base-process-container}/UIPolicy_Schema.md (100%) rename docs/{ => lxc-support}/lxc-backend.md (100%) rename docs/{ => nanvix-microvm}/nanvix-integration-plan.md (99%) rename docs/{microvm.md => nanvix-microvm/nanvix.md} (100%) rename docs/{ => windows-sandbox}/windows-sandbox-reference.md (100%) rename docs/{ => windows-sandbox}/windows-sandbox.md (100%) diff --git a/Readme.md b/Readme.md index 94af021d1..51f3e02af 100644 --- a/Readme.md +++ b/Readme.md @@ -156,7 +156,7 @@ xperf -merge user.etl kernel.etl merged.etl MXC also supports Linux via [LXC (Linux Containers)](https://linuxcontainers.org/lxc/). On Linux, the `lxc-exec` binary provides container-based isolation using Linux namespaces, bind mounts for filesystem policy, and iptables/nftables for network policy. -For full details on the LXC backend, see [docs/lxc-backend.md](docs/lxc-backend.md). +For full details on the LXC backend, see [docs/lxc-support/lxc-backend.md](docs/lxc-support/lxc-backend.md). ### Building on Linux diff --git a/docs/UIPolicy_Schema.md b/docs/base-process-container/UIPolicy_Schema.md similarity index 100% rename from docs/UIPolicy_Schema.md rename to docs/base-process-container/UIPolicy_Schema.md diff --git a/docs/lxc-backend.md b/docs/lxc-support/lxc-backend.md similarity index 100% rename from docs/lxc-backend.md rename to docs/lxc-support/lxc-backend.md diff --git a/docs/nanvix-integration-plan.md b/docs/nanvix-microvm/nanvix-integration-plan.md similarity index 99% rename from docs/nanvix-integration-plan.md rename to docs/nanvix-microvm/nanvix-integration-plan.md index 094224f8c..a7b43ee60 100644 --- a/docs/nanvix-integration-plan.md +++ b/docs/nanvix-microvm/nanvix-integration-plan.md @@ -114,7 +114,7 @@ mxc/src/ ├── wxc_windows_sandbox_guest/ # UNCHANGED └── wxc_windows_sandbox_daemon/ # UNCHANGED -mxc/docs/ +mxc/docs/nanvix-microvm/ └── nanvix-integration-plan.md # NEW — this document mxc/test_configs/ diff --git a/docs/microvm.md b/docs/nanvix-microvm/nanvix.md similarity index 100% rename from docs/microvm.md rename to docs/nanvix-microvm/nanvix.md diff --git a/docs/windows-sandbox-reference.md b/docs/windows-sandbox/windows-sandbox-reference.md similarity index 100% rename from docs/windows-sandbox-reference.md rename to docs/windows-sandbox/windows-sandbox-reference.md diff --git a/docs/windows-sandbox.md b/docs/windows-sandbox/windows-sandbox.md similarity index 100% rename from docs/windows-sandbox.md rename to docs/windows-sandbox/windows-sandbox.md diff --git a/sdk/README.md b/sdk/README.md index d13aca133..62bb52d3e 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -1,620 +1,382 @@ -# MXC SDK +# `@microsoft/mxc-sdk` -> **Status: Public Preview** - MXC is experimental and in active development. +> Node.js / TypeScript SDK for **MXC** (Microsoft eXecution Containers) — a policy-driven sandbox for running untrusted code (model output, plugins, tools) on Windows, Linux, and macOS. -## Overview - -The MXC SDK provides a Node.js interface for creating and managing policy-based containers. It exposes APIs for: - -- Defining container policies (filesystem, network) -- Discovering host tools and helpers for building the policy -- Spawning containerized processes with full interactive I/O via node-pty - -## Features - -- **Platform Detection**: Check if MXC is supported on the current system -- **Policy-Driven Configuration**: Define what the container can access using a `SandboxPolicy` -- **Policy Discovery**: Automatically discover host tools, user profile paths, and temp directories to build the policy -- **Interactive Process Spawning**: Spawn containerized processes with full PTY I/O using node-pty -- **Cross-Platform**: Process containment for Windows and Linux -- **TypeScript Support**: Full type definitions for all public APIs - -## Installation - -### From a tarball - -```bash -npm install @microsoft/mxc-sdk-.tgz -``` - -### From source +> **Status: Public Preview.** Schemas and APIs may change between minor versions until 1.0. ```bash -cd sdk -npm install -npm run build +npm install @microsoft/mxc-sdk ``` -Then reference it from your project (e.g., via `npm link` or a relative path in `package.json`). - -**Requirements**: -- **Windows**: Windows 11 build 26100+ with UBR ≥ 7965 (for builds 26100–26500) -- **Windows**: The host process must run **elevated (Administrator)** to create AppContainer-based sandboxes -- **Linux**: LXC must be installed and available - -**Platform Support**: -- ✅ Windows x64 -- ✅ Windows ARM64 -- ✅ Linux x64 -- ✅ Linux ARM64 -- ❌ macOS (not supported) - -> **Note**: The SDK automatically detects the platform and architecture. - -> **Note**: Use `getPlatformSupport()` to check if your system meets all requirements before attempting to create containers. - -## Quick Start - ```typescript import { - spawnSandbox, - SandboxPolicy, + spawnSandboxFromConfig, createConfigFromPolicy, + getAvailableToolsPolicy, getTemporaryFilesPolicy, getPlatformSupport, - getAvailableToolsPolicy, - getTemporaryFilesPolicy, } from '@microsoft/mxc-sdk'; -// Check platform support -const support = getPlatformSupport(); -if (!support.isSupported) { - console.error('MXC is not supported:', support.reason); - process.exit(1); +if (!getPlatformSupport().isSupported) { + throw new Error('MXC not available on this host'); } -// Discover host tools and temp directories +// Discover host tools (python, node, etc.) and a writable temp dir. const tools = getAvailableToolsPolicy(process.env); -const temp = getTemporaryFilesPolicy(); +const temp = getTemporaryFilesPolicy(); -// Define a sandbox policy -const policy: SandboxPolicy = { - version: '0.4.0-alpha', +const config = createConfigFromPolicy({ + version: '0.5.0-alpha', filesystem: { - readonlyPaths: tools.readonlyPaths, - readwritePaths: temp.readwritePaths, + readonlyPaths: tools.readonlyPaths, // PATH, PYTHONPATH, JAVA_HOME, … + readwritePaths: temp.readwritePaths, // %TEMP% / $TMPDIR }, - network: { - allowOutbound: true, - }, -}; - -// Spawn a sandboxed payload -const ptyProcess = spawnSandbox('python -c "print(\'Hello from sandbox!\')"', policy); - -// Handle output -ptyProcess.onData((data: string) => { - process.stdout.write(data); + network: { allowOutbound: false }, + timeoutMs: 30_000, }); +config.process!.commandLine = 'python -c "print(\'hello from sandbox\')"'; -// Handle exit -ptyProcess.onExit((event: { exitCode: number }) => { - console.log(`Process exited with code ${event.exitCode}`); -}); +const child = spawnSandboxFromConfig(config, { usePty: false }); +child.stdout!.on('data', (d) => process.stdout.write(d)); +child.on('close', (code) => console.log('exit:', code)); ``` -## API Reference +--- -### Platform Detection +## Compatibility -#### Containment values: intent vs. backend + -The SDK distinguishes two layers of containment values: +**Policy / config schema versions:** -- **`ContainmentType`** — abstract intent (what *kind* of isolation you want). - Currently `"process"`, `"vm"`, and `"microvm"`. The native binary resolves - these to a concrete backend per host. Prefer these in policy code so the - same policy works across hosts with different capabilities. -- **`ContainmentBackend`** — concrete backend (a specific runner). Currently - `"processcontainer"`, `"windows_sandbox"`, `"wslc"`, `"lxc"`, `"microvm"`, - `"seatbelt"`. Use these to force a particular backend. +| Version | Status | Schema file | +| --- | --- | --- | +| `0.4.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.4.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.4.0-alpha.json) | +| `0.5.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.5.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.5.0-alpha.json) | +| `0.6.0-dev` | Dev (latest features: macOS / Seatbelt, etc.) | [`schemas/dev/mxc-config.schema.0.6.0-dev.json`](https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.6.0-dev.json) | -`ContainerConfig.containment` accepts either layer. The deprecated -`SandboxingMethod` alias is the union of both and is retained for backward -compatibility. +Pick `0.5.0-alpha` for new code targeting Windows / Linux; pick `0.6.0-dev` if you need macOS or the newest experimental features. Full model: [`docs/versioning.md`](https://github.com/microsoft/mxc/blob/main/docs/versioning.md). -#### `getPlatformSupport(): PlatformSupport` +> **Heads-up for `0.4.0-alpha` on Windows:** firewall network enforcement (`network.blockedHosts` — the only one currently wired through on `0.4.0-alpha`; `allowedHosts` is not effective) and `network.proxy` both require the host process to be elevated (Administrator) — they hit Windows APIs that need it and may surface a UAC prompt. Hosts must be specified as IP addresses (DNS-based filtering isn't supported on `0.4.0-alpha`). `0.5.0-alpha`+ moved proxy handling in-OS, so the elevation requirement is gone there (firewall enforcement on `0.5.0-alpha`+ is not yet implemented). All other policy fields work unelevated on every schema version. -Returns platform support information including whether MXC is supported. - -```typescript -import { getPlatformSupport } from '@microsoft/mxc-sdk'; +**Platforms:** -const support = getPlatformSupport(); -console.log('Supported:', support.isSupported); -console.log('Available methods:', support.availableMethods); +| Platform | Default backend | Other backends | +| --- | --- | --- | +| Windows 11 build 26100+ (UBR ≥ 7965) | `processcontainer` | `windows_sandbox`, `wslc`, `microvm`, `isolation_session` | +| Linux x64 / ARM64 | `lxc` | — | +| macOS x64 / ARM64 (schema `0.6.0-dev`+) | `seatbelt` | — | -if (support.reason) { - console.log('Reason:', support.reason); -} -``` +The default `processcontainer` and `lxc` backends work out of the box. **Experimental backends** (`windows_sandbox`, `wslc`, `microvm`, `seatbelt`, `isolation_session`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). -**Return type**: +**Node.js:** ≥ 18. -```typescript -interface PlatformSupport { - isSupported: boolean; - reason?: string; - availableMethods: ContainmentBackend[]; -} -``` +--- -**Example outputs**: +## Three Ways to Spawn -Supported system: -``` -Supported: true -Available methods: ['processcontainer'] -``` +The SDK provides three entry points. **Prefer the config-based path** (`createConfigFromPolicy` + `spawnSandboxFromConfig`) — it gives you backend selection, backend-specific tuning, and (with `usePty: false`) separated stdout/stderr. -Unsupported system: -``` -Supported: false -Reason: MXC is not supported on macOS -Available methods: [] -``` +### 1. Config-based — recommended -### Sandbox Spawning +```typescript +import { + createConfigFromPolicy, spawnSandboxFromConfig, + getAvailableToolsPolicy, getTemporaryFilesPolicy, +} from '@microsoft/mxc-sdk'; -#### `spawnSandbox(script, policy, options?, workingDirectory?, containerName?, env?): IPty` +const tools = getAvailableToolsPolicy(process.env); +const temp = getTemporaryFilesPolicy(); + +const config = createConfigFromPolicy( + { + version: '0.5.0-alpha', + filesystem: { + readonlyPaths: tools.readonlyPaths, + readwritePaths: temp.readwritePaths, + }, + network: { allowOutbound: true }, + timeoutMs: 30_000, + }, + 'process', // intent: "process" | "vm" | "microvm" +); -Spawns a containerized process and returns a node-pty `IPty` object for interactive I/O. +// Add the script and any backend-specific runtime settings on the returned config. +config.process!.commandLine = 'python script.py'; -**Parameters**: -- `script` (`string`): The command line to execute inside the container -- `policy` (`SandboxPolicy`): The sandbox policy defining container permissions -- `options` (`SandboxSpawnOptions`, optional): Spawn options - - `debug`: Enable debug output (default: `false`) - - `ptyOptions`: node-pty options (cols, rows, etc.) -- `workingDirectory` (`string`, optional): Working directory for the process -- `containerName` (`string`, optional): Container name (auto-generated if omitted) -- `env` (`object`, optional): Environment variables to pass to the container +// PTY mode (default) — IPty, merged stdout+stderr +const pty = spawnSandboxFromConfig(config); +pty.onData((d) => process.stdout.write(d)); +pty.onExit(({ exitCode }) => console.log('exit:', exitCode)); + +// Pipe mode — ChildProcess with separated stdout/stderr + reliable exit codes +const child = spawnSandboxFromConfig(config, { usePty: false }); +child.stdout!.on('data', (d) => process.stdout.write(d)); +child.stderr!.on('data', (d) => process.stderr.write(d)); +child.on('close', (code) => console.log('exit:', code)); +``` -**Returns**: `IPty` object for interacting with the containerized process +### 2. `spawnSandbox(script, policy, ...)` — convenience -**Throws**: Error if platform is not supported +Quick path for **process-isolation only** (`processcontainer` on Windows, `lxc` on Linux, `seatbelt` on macOS). Returns a `node-pty` `IPty` with merged stdout/stderr. ```typescript -import { spawnSandbox, SandboxPolicy, getAvailableToolsPolicy } from '@microsoft/mxc-sdk'; +import { + spawnSandbox, + getAvailableToolsPolicy, getTemporaryFilesPolicy, +} from '@microsoft/mxc-sdk'; const tools = getAvailableToolsPolicy(process.env); +const temp = getTemporaryFilesPolicy(); -const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { readonlyPaths: tools.readonlyPaths }, - network: { allowOutbound: true }, -}; - -const ptyProcess = spawnSandbox( - 'python -c "print(\'Hello!\')"', - policy, - { debug: true, ptyOptions: { cols: 120, rows: 40 } }, -); - -ptyProcess.onData((data) => console.log(data)); -ptyProcess.onExit((event) => console.log('Exit code:', event.exitCode)); +const pty = spawnSandbox('python script.py', { + version: '0.5.0-alpha', + filesystem: { + readonlyPaths: tools.readonlyPaths, + readwritePaths: temp.readwritePaths, + }, + timeoutMs: 30_000, +}); +pty.onData((d) => process.stdout.write(d)); +pty.onExit(({ exitCode }) => console.log('exit:', exitCode)); ``` -#### `spawnSandboxAsync(script, policy, options?, workingDirectory?, containerName?): Promise<...>` +### 3. `spawnSandboxAsync(script, policy, ...)` — promise-style -Spawns a containerized process and returns a promise that resolves with the collected output. Convenience wrapper for non-interactive use cases. - -**Returns**: `Promise<{ stdout: string; stderr: string; exitCode: number }>` +The `await`-friendly version of `spawnSandbox`. Same arguments, same restriction (process-isolation only), but resolves with `{ stdout, stderr, exitCode }` instead of returning an `IPty`. `stderr` is always `''` because the underlying PTY merges streams. ```typescript -import { spawnSandboxAsync, SandboxPolicy, getAvailableToolsPolicy } from '@microsoft/mxc-sdk'; +import { + spawnSandboxAsync, + getAvailableToolsPolicy, getTemporaryFilesPolicy, +} from '@microsoft/mxc-sdk'; const tools = getAvailableToolsPolicy(process.env); - -const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { readonlyPaths: tools.readonlyPaths }, -}; +const temp = getTemporaryFilesPolicy(); const result = await spawnSandboxAsync( 'python -c "import sys; print(sys.version)"', - policy, + { + version: '0.5.0-alpha', + filesystem: { + readonlyPaths: tools.readonlyPaths, + readwritePaths: temp.readwritePaths, + }, + timeoutMs: 30_000, + }, ); - -console.log('Output:', result.stdout); -console.log('Exit code:', result.exitCode); -``` - -### Policy Discovery - -These functions examine the host environment and return `FilesystemPolicyResult` fragments that can be merged into a `SandboxPolicy`. - -```typescript -interface FilesystemPolicyResult { - readonlyPaths: string[]; - readwritePaths: string[]; -} +console.log(result.stdout); ``` -#### `getAvailableToolsPolicy(env?, options?): FilesystemPolicyResult` +> **Tip:** for agentic workloads, prefer **multiple narrow sandboxes** (one policy per task step) over a single broad policy. Add task-specific paths on top of the discovered base (e.g. a scoped output directory in `readwritePaths`, a project source tree in `readonlyPaths`, secrets in `deniedPaths`). -Discovers tool and SDK directories from `PATH` and well-known environment variables (e.g., `PYTHONPATH`, `JAVA_HOME`, `CARGO_HOME`, `GOPATH`, etc.) and returns them as read-only policy paths. +--- -Filters out non-existent directories and system-critical paths (e.g., under `%WINDIR%`). +## Choosing a Backend -```typescript -import { getAvailableToolsPolicy } from '@microsoft/mxc-sdk'; +
+Table of all backends and links to per-backend guides — click to expand. -const toolsPolicy = getAvailableToolsPolicy(process.env); -console.log('Read-only tool paths:', toolsPolicy.readonlyPaths); -``` +`SandboxPolicy` is cross-platform. The backend is selected by the second argument to `createConfigFromPolicy(policy, containment)`. Pass an **abstract intent** (`"process"`, `"vm"`, `"microvm"`) whenever possible — the SDK and native binary resolve it to the right concrete backend for the host. Pass a **concrete backend name** when you need a specific runner. -#### `getUserProfilePolicy(): FilesystemPolicyResult` +| Backend | Intent | Platforms | Stable? | Guide | +| --- | --- | --- | --- | --- | +| `processcontainer` | `process` | Windows | ✅ | [`docs/base-process-container/guide.md`](https://github.com/microsoft/mxc/blob/main/docs/base-process-container/guide.md) | +| `lxc` | `process` | Linux | ✅ | [`docs/lxc-support/lxc-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/lxc-support/lxc-backend.md) | +| `seatbelt` | `process` | macOS | Experimental (schema `0.6.0-dev`+) | [`docs/macos-support/seatbelt-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/macos-support/seatbelt-backend.md) | +| `windows_sandbox` | `vm` | Windows | Experimental | [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) | +| `microvm` | `microvm` | Windows | Experimental | [`docs/nanvix-microvm/nanvix.md`](https://github.com/microsoft/mxc/blob/main/docs/nanvix-microvm/nanvix.md) — MicroVM via NanVix on Windows Hypervisor Platform | +| `wslc` | (concrete only) | Windows | Experimental | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | +| `isolation_session` | (concrete only) | Windows | Experimental | [`docs/isolation-session/initial-bringup-plan.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/initial-bringup-plan.md) | -Returns read-only policy paths for user profile application data. On Windows, enumerates subdirectories under `%LOCALAPPDATA%\Programs`. On Linux, includes `~/.local/bin` and `~/.local/lib`. +Experimental backends require `{ experimental: true }` in `SandboxSpawnOptions`: ```typescript -import { getUserProfilePolicy } from '@microsoft/mxc-sdk'; - -const profilePolicy = getUserProfilePolicy(); -console.log('User profile paths:', profilePolicy.readonlyPaths); +const config = createConfigFromPolicy(policy, 'vm'); // → windows_sandbox on Windows +config.process!.commandLine = 'cmd /c whoami'; +const pty = spawnSandboxFromConfig(config, { experimental: true }); ``` -#### `getTemporaryFilesPolicy(env?): FilesystemPolicyResult` - -Returns a read-write policy path for the system temporary directory (`%TEMP%` on Windows, `$TMPDIR` or `/tmp` on Linux). - -```typescript -import { getTemporaryFilesPolicy } from '@microsoft/mxc-sdk'; - -const tempPolicy = getTemporaryFilesPolicy(); -console.log('Temp paths:', tempPolicy.readwritePaths); -``` +Backend-specific tuning lives on the returned `ContainerConfig`. The full set of fields per backend is in the JSON schemas — they're the source of truth: -## Policy +- Stable backends: [`schemas/stable/`](https://github.com/microsoft/mxc/tree/main/schemas/stable/) +- Experimental backends: [`schemas/dev/`](https://github.com/microsoft/mxc/tree/main/schemas/dev/) -### SandboxPolicy +Open the schema file matching your `policy.version` (e.g. `mxc-config.schema.0.5.0-alpha.json`) and look up `processContainer`, `lxc`, `experimental.wslc`, `experimental.windows_sandbox`, etc. -The `SandboxPolicy` type is the public interface for defining what a sandboxed payload is allowed to do. Policy describes *what* the caller wants restricted — cross-platform, no OS-specific content. Omitted fields default to most restrictive (default-deny). The SDK translates this into the internal container configuration automatically via `createConfigFromPolicy()`. +
-```typescript -type SandboxPolicy = { - version: string; - - filesystem?: { - readwritePaths?: string[]; - readonlyPaths?: string[]; - deniedPaths?: string[]; - clearPolicyOnExit?: boolean; - }; - - network?: { - allowOutbound?: boolean; - allowLocalNetwork?: boolean; - allowedHosts?: string[]; - blockedHosts?: string[]; - proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; - }; - - ui?: { - allowWindows?: boolean; - clipboard?: "none" | "read" | "write" | "all"; - allowInputInjection?: boolean; - }; - - timeoutMs?: number; -}; -``` +## State-Aware Sandboxes -> **Note**: Low-level container options are managed internally by the SDK based on the policy and platform. Use the advanced path (`createConfigFromPolicy()` → modify → `spawnSandboxFromConfig()`) if you need to tweak backend-specific settings. +
+Provision once, exec many, tear down (long-lived workflows) — click to expand. -### Merging Policy Fragments +For long-lived sandboxes where you provision once, exec many times, and tear down at the end (e.g. agentic loops), use the state-aware lifecycle. -Combine the policy discovery functions to build a complete policy: +> **Backend support:** the state-aware lifecycle is currently only implemented for `isolation_session` (Windows). The one-shot spawn APIs (`spawnSandbox` / `spawnSandboxFromConfig`) are the supported path for every other backend. ```typescript import { - SandboxPolicy, - getAvailableToolsPolicy, - getUserProfilePolicy, - getTemporaryFilesPolicy, - spawnSandbox, + provisionSandbox, startSandbox, execInSandboxAsync, + stopSandbox, deprovisionSandbox, } from '@microsoft/mxc-sdk'; -const tools = getAvailableToolsPolicy(process.env); -const profile = getUserProfilePolicy(); -const temp = getTemporaryFilesPolicy(); - -const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { - readonlyPaths: [...tools.readonlyPaths, ...profile.readonlyPaths], - readwritePaths: [...temp.readwritePaths, 'C:\\workspace\\output'], - deniedPaths: ['C:\\secrets'], - }, - network: { - allowOutbound: true, - }, -}; - -const ptyProcess = spawnSandbox('python script.py', policy, {}, 'C:\\workspace'); -``` - -## Examples - -### Minimal — Run a Command - -```typescript -import { spawnSandbox, SandboxPolicy, getAvailableToolsPolicy } from '@microsoft/mxc-sdk'; - -const tools = getAvailableToolsPolicy(process.env); - -const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { readonlyPaths: tools.readonlyPaths }, -}; - -const ptyProcess = spawnSandbox('python -c "print(\'Hello World\')"', policy); - -ptyProcess.onData((data) => process.stdout.write(data)); -ptyProcess.onExit(() => console.log('Done!')); -``` +const { sandboxId } = await provisionSandbox('isolation_session'); +await startSandbox(sandboxId); -### Network — Allow Outbound Access +const r1 = await execInSandboxAsync(sandboxId, { process: { commandLine: 'echo hello' } }); +const r2 = await execInSandboxAsync(sandboxId, { process: { commandLine: 'whoami' } }); -```typescript -import { spawnSandboxAsync, SandboxPolicy, getAvailableToolsPolicy } from '@microsoft/mxc-sdk'; - -const tools = getAvailableToolsPolicy(process.env); - -const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { readonlyPaths: tools.readonlyPaths }, - network: { allowOutbound: true }, -}; - -const result = await spawnSandboxAsync( - 'python -c "import urllib.request; print(urllib.request.urlopen(\'https://api.github.com\').read())"', - policy, -); -console.log(result.stdout); +await stopSandbox(sandboxId); +await deprovisionSandbox(sandboxId); ``` -### Filesystem — Restrict Access - -```typescript -import { spawnSandbox, SandboxPolicy, getAvailableToolsPolicy } from '@microsoft/mxc-sdk'; +Full design and API: [`docs/state-aware-lifecycle/`](https://github.com/microsoft/mxc/tree/main/docs/state-aware-lifecycle/). -const tools = getAvailableToolsPolicy(process.env); +
-const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { - readonlyPaths: [...tools.readonlyPaths, 'C:\\projects\\myapp\\config'], - readwritePaths: ['C:\\projects\\myapp\\data'], - deniedPaths: ['C:\\Windows\\System32'], - }, -}; +## Policy Discovery Helpers -const ptyProcess = spawnSandbox('python script.py', policy, {}, 'C:\\projects\\myapp'); -``` +
+Auto-enumerate host tools, profile, and temp dirs — click to expand. -### Combined — Fetch from Web and Write to Disk +The SDK ships helpers that enumerate the host environment so your policy stays portable: ```typescript import { - SandboxPolicy, - getAvailableToolsPolicy, - getTemporaryFilesPolicy, - spawnSandboxAsync, + getAvailableToolsPolicy, getUserProfilePolicy, getTemporaryFilesPolicy, } from '@microsoft/mxc-sdk'; -const tools = getAvailableToolsPolicy(process.env); -const temp = getTemporaryFilesPolicy(); +const tools = getAvailableToolsPolicy(process.env); // PATH, PYTHONPATH, JAVA_HOME, … +const profile = getUserProfilePolicy(); // %LOCALAPPDATA%\Programs, ~/.local/* +const tmp = getTemporaryFilesPolicy(); // %TEMP% / $TMPDIR -const policy: SandboxPolicy = { - version: '0.4.0-alpha', +const policy = { + version: '0.5.0-alpha', filesystem: { - readonlyPaths: tools.readonlyPaths, - readwritePaths: [...temp.readwritePaths, 'C:\\workspace\\output'], + readonlyPaths: [...tools.readonlyPaths, ...profile.readonlyPaths], + readwritePaths: tmp.readwritePaths, }, - network: { allowOutbound: true }, + network: { allowOutbound: false }, }; +``` -// Python script that fetches JSON from an API and writes it to a local file -const script = `python -c " -import urllib.request, json, os - -url = 'https://api.github.com/zen' -response = urllib.request.urlopen(url) -wisdom = response.read().decode('utf-8') - -output_dir = r'C:\\workspace\\output' -os.makedirs(output_dir, exist_ok=True) -output_path = os.path.join(output_dir, 'zen.txt') - -with open(output_path, 'w') as f: - f.write(wisdom) - -print(f'Wrote GitHub zen to {output_path}: {wisdom}') -"`; +Each helper returns `{ readonlyPaths, readwritePaths }` — merge what you want into `SandboxPolicy.filesystem`. -const result = await spawnSandboxAsync(script, policy, {}, 'C:\\workspace'); +
-console.log('Output:', result.stdout); -console.log('Exit code:', result.exitCode); -``` +--- ## Common Pitfalls -### `spawnSandboxAsync` merges stdout and stderr +### UI is blocked by default on 0.5.0+ — some shells need it -`spawnSandboxAsync` uses a PTY internally. **stderr is always empty** in the returned result — all output (including error output) arrives in `stdout`. If you need to distinguish stderr from stdout, you must handle it at the application level inside the sandbox (e.g., redirect stderr to a file, then read it back). +The `policy.ui` block is enforced starting with schema `0.5.0-alpha` (it has no effect on `0.4.0-alpha`). When you use `0.5.0-alpha` or `0.6.0-dev`, `policy.ui.allowWindows` defaults to `false`. Most non-interactive command-line tools work fine, but on Windows some shells make win32k system calls during startup and fail without UI access. **All versions of PowerShell are affected** — both Windows PowerShell 5.1 (`powershell.exe`) and PowerShell 7 (`pwsh.exe`). Set `ui.allowWindows: true` when launching a shell: ```typescript -// result.stderr will always be '' — this is by design -const result = await spawnSandboxAsync('python script.py', policy); -console.log(result.stdout); // Contains both stdout AND stderr -console.log(result.stderr); // Always empty string -``` - -### `createConfigFromPolicy()` returns an empty command line - -If you use the advanced path (`createConfigFromPolicy()` → modify → `spawnSandboxFromConfig()`), the returned config has an **empty `process.commandLine`**. You must set it before spawning: +import { spawnSandboxFromConfig, createConfigFromPolicy } from '@microsoft/mxc-sdk'; -```typescript -import { createConfigFromPolicy, spawnSandboxFromConfig } from '@microsoft/mxc-sdk'; +const config = createConfigFromPolicy({ + version: '0.5.0-alpha', + ui: { allowWindows: true }, // ← required for powershell.exe to start +}); +config.process!.commandLine = 'powershell.exe -NoProfile -Command "Get-Date"'; -const config = createConfigFromPolicy(policy); -config.process.commandLine = 'python -c "print(\'hello\')"'; // ← Required! -const pty = spawnSandboxFromConfig(config); +const child = spawnSandboxFromConfig(config, { usePty: false }); ``` -### Elevation failures on Windows +### PTY APIs merge stdout and stderr -If the host process is not elevated, `spawnSandbox` will throw. The error message may not immediately indicate a privilege issue. Always ensure your host process runs as Administrator during development and testing. +`spawnSandbox` and `spawnSandboxAsync` use a PTY, so `stderr` is always empty in their result. Use `spawnSandboxFromConfig(config, { usePty: false })` for separated streams. -### Timeouts default to no limit +### `createConfigFromPolicy` leaves `commandLine` empty -If you delegate work to a sandbox, set `timeoutMs` in your policy to prevent runaway processes: +You must set `config.process!.commandLine = '…'` before calling `spawnSandboxFromConfig`. -```typescript -const policy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { readonlyPaths: tools.readonlyPaths }, - timeoutMs: 30000, // 30 seconds -}; -``` +### Default-deny applies to everything -## Recommended Policy for Agentic Workloads +No `network` field → no network. No `readwritePaths` → process can't write `%TEMP%`. No `ui` → no GUI. Use the discovery helpers to compose a sensible baseline. -Agents that execute code on behalf of a user typically need scoped filesystem access and limited (or no) network. The `SandboxPolicy` model is **default-deny** — omitted fields are restrictive. +### `process.cwd` doesn't grant filesystem access -```typescript -import { - SandboxPolicy, - getAvailableToolsPolicy, - getTemporaryFilesPolicy, -} from '@microsoft/mxc-sdk'; +Setting `cwd` (or the `workingDirectory` argument) does **not** add that path to the policy. Add it to `readonlyPaths` / `readwritePaths` explicitly. -const tools = getAvailableToolsPolicy(process.env); -const temp = getTemporaryFilesPolicy(); +--- -const agentPolicy: SandboxPolicy = { - version: '0.4.0-alpha', - filesystem: { - readonlyPaths: [ - ...tools.readonlyPaths, - // Add paths to project files the agent needs to read - ], - readwritePaths: [ - ...temp.readwritePaths, - // Add a scoped output directory for agent artifacts - ], - }, - network: { - // Only enable if the agent needs external API access - allowOutbound: false, - // Or restrict to specific hosts: - // allowedHosts: ['api.example.com'], - }, - timeoutMs: 60000, // Prevent runaway execution -}; -``` - -**Principle:** Grant the sandbox only what the current task requires. If your agent performs multiple steps, consider spawning separate sandboxes with different policies for each step rather than one broad policy. - -## Error Handling +## Troubleshooting -```typescript -import { getPlatformSupport, spawnSandboxAsync } from '@microsoft/mxc-sdk'; +
+Common errors and what they mean — click to expand. -// 1. Check platform before any sandbox operations -const support = getPlatformSupport(); -if (!support.isSupported) { - // Degrade gracefully — run without containment or inform the user - console.error(`MXC not available: ${support.reason}`); -} +| Error | Cause | Fix | +| --- | --- | --- | +| `MXC is not supported on this platform` | `getPlatformSupport()` returned `isSupported: false`. On Windows: build < 26100 or UBR < 7965. On Linux: `lxc-ls` not on PATH. macOS: schema version < `0.6.0-dev`. | Update the OS, install LXC, or switch to schema `0.6.0-dev`. | +| `wxc-exec.exe not found` / `lxc-exec not found` | The SDK couldn't locate the native binary. | Set `MXC_BIN_DIR=` so `//wxc-exec.exe` (or `lxc-exec`) exists, or pass `options.executablePath` explicitly. | +| `Invalid containment value ''` | `containment` field doesn't match the parser's accepted values. | Use one of the abstract intents (`process`, `vm`, `microvm`) or a concrete backend listed in [Choosing a Backend](#choosing-a-backend). | +| `'' containment requires experimental mode` | A `windows_sandbox` / `wslc` / `microvm` / `seatbelt` / `isolation_session` backend was selected without the flag. | Pass `{ experimental: true }` in `SandboxSpawnOptions`. | +| `process.commandLine starts with an unquoted Windows path containing a space` | `wxc-exec` rejects unquoted paths with spaces at parse time. | Quote the executable: `'"C:\\Program Files\\…\\foo.exe" args'`. | +| `Experimental_CreateProcessInSandbox failed: WIN32_ERROR(...)` | Native sandbox API returned an OS-level error. Codes vary: `120` = call not implemented (BaseContainer disabled — use schema `0.4.0-alpha`), `448` = device feature not supported (Windows build / WIP feature not enabled). | Check the Windows build / WIP requirements, or fall back to schema `0.4.0-alpha`. | +| Process exits `-1` / `4294967295` with no stdout | Native binary terminated abnormally. | Re-run with `options.debug: true` (or `options.logDir: ''`) to capture diagnostic logs. | +| `policy.version '' is older than supported` / `newer than supported` | Version is outside the SDK's accepted range. | Use `0.4.0-alpha`, `0.5.0-alpha`, or `0.6.0-dev`. See [Compatibility](#compatibility). | -// 2. Wrap spawn calls in try/catch -try { - const result = await spawnSandboxAsync(command, policy); - if (result.exitCode !== 0) { - // Non-zero exit — check result.stdout for error output (stderr is merged) - handleFailure(result.stdout, result.exitCode); - } -} catch (err) { - // Spawn failures: privilege issues, invalid policy, missing binaries - handleSpawnError(err); -} -``` +For backend-specific errors, see the per-backend guide linked from the [Choosing a Backend](#choosing-a-backend) table. -## Integration Checklist +
-- [ ] `getPlatformSupport()` called at startup; graceful fallback if unsupported -- [ ] Host process runs elevated (Administrator) on Windows -- [ ] Policy uses default-deny; only required paths/network are enabled -- [ ] `timeoutMs` set to prevent unbounded execution -- [ ] `spawnSandboxAsync` stdout handled knowing stderr is merged -- [ ] Error handling covers both spawn failures and non-zero exit codes -- [ ] Tested on Windows 11 build 26100+ with UBR ≥ 7965 +--- -## TypeScript Support +## API Surface -The package includes full TypeScript definitions. All public types are exported from the main entry point: +
+Every export at a glance — click to expand. ```typescript -import { - // Types - SandboxPolicy, - ContainmentType, - ContainmentBackend, - SandboxingMethod, // deprecated alias for ContainmentType | ContainmentBackend - PlatformSupport, - - // Platform detection - getPlatformSupport, - - // Sandbox spawning - spawnSandbox, - spawnSandboxAsync, - SandboxSpawnOptions, - - // Policy discovery - getAvailableToolsPolicy, - getUserProfilePolicy, - getTemporaryFilesPolicy, - FilesystemPolicyResult, - ToolsPolicyOptions, -} from '@microsoft/mxc-sdk'; +// Spawn — config-based (recommended) +createConfigFromPolicy(policy, containment?, containerName?) → ContainerConfig +spawnSandboxFromConfig(config, options?, workingDirectory?, env?) → IPty | ChildProcess + +// Spawn — convenience (process containment only) +spawnSandbox(script, policy, options?, workingDirectory?, containerName?, env?) → IPty +spawnSandboxAsync(script, policy, ...) → Promise<{ stdout, stderr, exitCode }> + +// State-aware lifecycle (currently only `isolation_session` on Windows) +provisionSandbox(containment, config?, options?) → Promise +startSandbox(sandboxId, config?, options?) → Promise +execInSandbox(sandboxId, config, options?) → IPty // streaming +execInSandboxAsync(sandboxId, config, options?) → Promise +stopSandbox(sandboxId, config?, options?) → Promise +deprovisionSandbox(sandboxId, config?, options?) → Promise + +// Platform & policy discovery +getPlatformSupport() → PlatformSupport +getAvailableToolsPolicy(env?, options?) → FilesystemPolicyResult +getUserProfilePolicy() → FilesystemPolicyResult +getTemporaryFilesPolicy(env?) → FilesystemPolicyResult + +// Errors (typed wire-format errors from wxc-exec) +ErrorCode, MxcError, mxcErrorFromCode(code) ``` -## Development +Full TypeScript definitions ship with the package (`dist/index.d.ts`). All exports are named exports from `@microsoft/mxc-sdk`. -```bash -# Install dependencies -npm install +
-# Build -npm run build +--- -# Run tests -npm test +## Further Reading -# Watch mode -npm run watch +- [`docs/schema.md`](https://github.com/microsoft/mxc/blob/main/docs/schema.md) — full configuration schema reference +- [`docs/versioning.md`](https://github.com/microsoft/mxc/blob/main/docs/versioning.md) — schema versioning model and experimental-feature lifecycle +- [`docs/examples.md`](https://github.com/microsoft/mxc/blob/main/docs/examples.md) — annotated configuration examples +- [`docs/sandbox-policy/v1/policy.md`](https://github.com/microsoft/mxc/blob/main/docs/sandbox-policy/v1/policy.md) — policy specification +- Backend-specific guides linked in the [Choosing a Backend](#choosing-a-backend) section above. -# Clean build artifacts -npm run clean -``` +--- ## License -See the [LICENSE](../LICENSE.md) file for details. - -## Contributing - -Contributions are welcome! Please see the main MXC project repository for contribution guidelines. +[MIT](https://github.com/microsoft/mxc/blob/main/sdk/LICENSE.md). Contributions welcome — see the main [MXC repository](https://github.com/microsoft/mxc). diff --git a/src/wxc_common/src/base_container_runner.rs b/src/wxc_common/src/base_container_runner.rs index d85c6bd78..402de2367 100644 --- a/src/wxc_common/src/base_container_runner.rs +++ b/src/wxc_common/src/base_container_runner.rs @@ -24,7 +24,8 @@ use windows_core::PCWSTR; use crate::logger::Logger; use crate::models::{ - CodexRequest, NetworkEnforcementMode, NetworkPolicy, ProxyAddress, ScriptResponse, + BaseProcessUiConfig, ClipboardPolicy, CodexRequest, NetworkEnforcementMode, NetworkPolicy, + ProxyAddress, ScriptResponse, UiPolicy, }; use crate::proxy_coordinator::ProxyCoordinator; use crate::script_runner::{get_timeout_milliseconds, ScriptRunner}; @@ -83,6 +84,95 @@ impl BaseContainerRunner { Self::load_api().map(|_| ()) } + /// JOB_OBJECT_UILIMIT_* flag constants (from base-process-container/UIPolicy_Schema.md). + const UILIMIT_HANDLES: u64 = 0x0001; + const UILIMIT_READCLIPBOARD: u64 = 0x0002; + const UILIMIT_WRITECLIPBOARD: u64 = 0x0004; + const UILIMIT_SYSTEMPARAMETERS: u64 = 0x0008; + const UILIMIT_DISPLAYSETTINGS: u64 = 0x0010; + const UILIMIT_GLOBALATOMS: u64 = 0x0020; + const UILIMIT_DESKTOP: u64 = 0x0040; + const UILIMIT_EXITWINDOWS: u64 = 0x0080; + const UILIMIT_IME: u64 = 0x0100; + const UILIMIT_INJECTION: u64 = 0x0200; + + /// Build the JOB_OBJECT_UILIMIT_* bitmask from the cross-platform UI policy + /// and the BaseProcessContainer-specific UI config. + /// Mapping follows docs/base-process-container/UIPolicy_Schema.md. + fn ui_restrictions_bitmask(ui: &UiPolicy, base_proc_ui: &BaseProcessUiConfig) -> u64 { + // When UI is fully disabled: DisallowWin32kSystemCalls handles everything + // except atoms (NT executive syscalls, not Win32k). Only set GLOBALATOMS. + if ui.disable { + return Self::UILIMIT_GLOBALATOMS; + } + + let mut mask: u64 = 0; + + // Cross-platform: clipboard (default: "none" = block both) + match ui.clipboard { + ClipboardPolicy::All => {} + ClipboardPolicy::Read => { + mask |= Self::UILIMIT_WRITECLIPBOARD; + } + ClipboardPolicy::Write => { + mask |= Self::UILIMIT_READCLIPBOARD; + } + // "none" or unrecognized → default-deny: block both + _ => { + mask |= Self::UILIMIT_READCLIPBOARD | Self::UILIMIT_WRITECLIPBOARD; + } + } + + // Cross-platform: input injection + if !ui.injection { + mask |= Self::UILIMIT_INJECTION; + } + + // Backend-specific: isolation level (default: "container" = HANDLES + GLOBALATOMS) + match base_proc_ui.isolation.as_str() { + "desktop" => { + // No isolation flags + } + "handles" => { + mask |= Self::UILIMIT_HANDLES; + } + "atoms" => { + mask |= Self::UILIMIT_GLOBALATOMS; + } + // "container" or unrecognized → default-deny: full isolation + _ => { + mask |= Self::UILIMIT_HANDLES | Self::UILIMIT_GLOBALATOMS; + } + } + + // Backend-specific: desktop system control + if !base_proc_ui.desktop_system_control { + mask |= Self::UILIMIT_DESKTOP | Self::UILIMIT_EXITWINDOWS; + } + + // Backend-specific: system settings (default: "none" = block all) + match base_proc_ui.system_settings.as_str() { + "all" => {} + "parameters" => { + mask |= Self::UILIMIT_DISPLAYSETTINGS; + } + "display" => { + mask |= Self::UILIMIT_SYSTEMPARAMETERS; + } + // "none" or unrecognized → default-deny: block all + _ => { + mask |= Self::UILIMIT_SYSTEMPARAMETERS | Self::UILIMIT_DISPLAYSETTINGS; + } + } + + // Backend-specific: IME + if !base_proc_ui.ime { + mask |= Self::UILIMIT_IME; + } + + mask + } + /// Build a FlatBuffer `SandboxSpec` from the container policy in the request. /// /// Maps `ContainerPolicy` and `UiPolicy` fields to the BaseContainer schema: @@ -174,12 +264,8 @@ impl BaseContainerRunner { }; // UI restrictions - let ui_restrictions = crate::job_object::to_job_object_uilimit_mask( - &crate::ui_policy::resolve_ui_restrictions( - &request.policy.ui, - &request.policy.base_process_ui, - ), - ) as u64; + let ui_restrictions = + Self::ui_restrictions_bitmask(&request.policy.ui, &request.policy.base_process_ui); let spec = SandboxSpec::create( &mut builder, @@ -306,11 +392,8 @@ impl ScriptRunner for BaseContainerRunner { let _ = writeln!(logger, "proxy URL in spec: {}", addr.to_url()); } - let restrictions = crate::ui_policy::resolve_ui_restrictions( - &request.policy.ui, - &request.policy.base_process_ui, - ); - let ui_restrictions = crate::job_object::to_job_object_uilimit_mask(&restrictions); + let ui_restrictions = + Self::ui_restrictions_bitmask(&request.policy.ui, &request.policy.base_process_ui); let _ = writeln!( logger, "sandbox spec built (version={}, {} bytes)", @@ -340,16 +423,16 @@ impl ScriptRunner for BaseContainerRunner { let _ = writeln!( logger, "UILIMIT flags: HANDLES={} READCLIP={} WRITECLIP={} SYSPARAM={} DISPLAY={} ATOMS={} DESKTOP={} EXIT={} IME={} INJECT={}", - restrictions.block_external_ui_objects, - restrictions.block_clipboard_read, - restrictions.block_clipboard_write, - restrictions.block_system_parameter_changes, - restrictions.block_display_settings_changes, - restrictions.block_global_ui_namespace, - restrictions.block_desktop_switching, - restrictions.block_logoff_or_shutdown, - restrictions.block_input_method_changes, - restrictions.block_input_injection, + ui_restrictions & Self::UILIMIT_HANDLES != 0, + ui_restrictions & Self::UILIMIT_READCLIPBOARD != 0, + ui_restrictions & Self::UILIMIT_WRITECLIPBOARD != 0, + ui_restrictions & Self::UILIMIT_SYSTEMPARAMETERS != 0, + ui_restrictions & Self::UILIMIT_DISPLAYSETTINGS != 0, + ui_restrictions & Self::UILIMIT_GLOBALATOMS != 0, + ui_restrictions & Self::UILIMIT_DESKTOP != 0, + ui_restrictions & Self::UILIMIT_EXITWINDOWS != 0, + ui_restrictions & Self::UILIMIT_IME != 0, + ui_restrictions & Self::UILIMIT_INJECTION != 0, ); let _ = writeln!(logger, "SECTION: Load API"); @@ -489,15 +572,9 @@ impl ScriptRunner for BaseContainerRunner { #[cfg(test)] mod tests { use super::*; - use crate::job_object::to_job_object_uilimit_mask; - use crate::models::{ClipboardPolicy, ProxyConfig, UiPolicy}; - use crate::ui_policy::EffectiveUiRestrictions; + use crate::models::ProxyConfig; use sandbox_spec::base_container_layout; - fn expected_mask(r: EffectiveUiRestrictions) -> u64 { - to_job_object_uilimit_mask(&r) as u64 - } - #[test] fn build_sandbox_spec_produces_valid_flatbuffer() { let mut request = CodexRequest::default(); @@ -521,14 +598,10 @@ mod tests { assert!(spec.least_privilege()); assert_eq!(spec.capabilities(), Some("internetClient,registryRead")); assert!(spec.disallow_win32k_system_calls()); - // default: disable=true → only the global UI namespace bit assert_eq!( spec.ui_restrictions(), - expected_mask(EffectiveUiRestrictions { - block_global_ui_namespace: true, - ..Default::default() - }) - ); + BaseContainerRunner::UILIMIT_GLOBALATOMS + ); // default: disable=true → only GLOBALATOMS let rw = spec.fs_read_write().unwrap(); assert_eq!(rw.len(), 1); @@ -575,6 +648,8 @@ mod tests { #[test] fn build_sandbox_spec_ui_disabled() { + use crate::models::UiPolicy; + let mut request = CodexRequest::default(); request.policy.ui = UiPolicy { disable: true, @@ -585,13 +660,10 @@ mod tests { let spec = base_container_layout::root_as_sandbox_spec(&bytes).unwrap(); assert!(spec.disallow_win32k_system_calls()); - // disable=true → only the global UI namespace bit (Win32k disable handles the rest) + // disable=true → only GLOBALATOMS (Win32k disable handles the rest) assert_eq!( spec.ui_restrictions(), - expected_mask(EffectiveUiRestrictions { - block_global_ui_namespace: true, - ..Default::default() - }) + BaseContainerRunner::UILIMIT_GLOBALATOMS ); } @@ -610,20 +682,15 @@ mod tests { assert!(!spec.disallow_win32k_system_calls()); // WRITECLIPBOARD + backend defaults (isolation=container: HANDLES+GLOBALATOMS, // desktopSystemControl=false: DESKTOP+EXITWINDOWS, systemSettings=none: SYSTEMPARAMETERS+DISPLAYSETTINGS, ime=false: IME) - assert_eq!( - spec.ui_restrictions(), - expected_mask(EffectiveUiRestrictions { - block_clipboard_write: true, - block_external_ui_objects: true, - block_global_ui_namespace: true, - block_desktop_switching: true, - block_logoff_or_shutdown: true, - block_system_parameter_changes: true, - block_display_settings_changes: true, - block_input_method_changes: true, - ..Default::default() - }) - ); + let expected = BaseContainerRunner::UILIMIT_WRITECLIPBOARD + | BaseContainerRunner::UILIMIT_HANDLES + | BaseContainerRunner::UILIMIT_GLOBALATOMS + | BaseContainerRunner::UILIMIT_DESKTOP + | BaseContainerRunner::UILIMIT_EXITWINDOWS + | BaseContainerRunner::UILIMIT_SYSTEMPARAMETERS + | BaseContainerRunner::UILIMIT_DISPLAYSETTINGS + | BaseContainerRunner::UILIMIT_IME; + assert_eq!(spec.ui_restrictions(), expected); } #[test] @@ -640,20 +707,15 @@ mod tests { assert!(!spec.disallow_win32k_system_calls()); // INJECTION + backend defaults - assert_eq!( - spec.ui_restrictions(), - expected_mask(EffectiveUiRestrictions { - block_input_injection: true, - block_external_ui_objects: true, - block_global_ui_namespace: true, - block_desktop_switching: true, - block_logoff_or_shutdown: true, - block_system_parameter_changes: true, - block_display_settings_changes: true, - block_input_method_changes: true, - ..Default::default() - }) - ); + let expected = BaseContainerRunner::UILIMIT_INJECTION + | BaseContainerRunner::UILIMIT_HANDLES + | BaseContainerRunner::UILIMIT_GLOBALATOMS + | BaseContainerRunner::UILIMIT_DESKTOP + | BaseContainerRunner::UILIMIT_EXITWINDOWS + | BaseContainerRunner::UILIMIT_SYSTEMPARAMETERS + | BaseContainerRunner::UILIMIT_DISPLAYSETTINGS + | BaseContainerRunner::UILIMIT_IME; + assert_eq!(spec.ui_restrictions(), expected); } #[test] @@ -682,4 +744,73 @@ mod tests { let spec = base_container_layout::root_as_sandbox_spec(&bytes).unwrap(); assert!(spec.network_policy().is_none()); } + + #[test] + fn ui_bitmask_disabled() { + use crate::models::BaseProcessUiConfig; + let ui = UiPolicy { + disable: true, + ..Default::default() + }; + let bp = BaseProcessUiConfig::default(); + // disable=true → only GLOBALATOMS + assert_eq!( + BaseContainerRunner::ui_restrictions_bitmask(&ui, &bp), + BaseContainerRunner::UILIMIT_GLOBALATOMS + ); + } + + #[test] + fn ui_bitmask_default_deny() { + use crate::models::BaseProcessUiConfig; + // UiPolicy default: disable=true → only GLOBALATOMS + assert_eq!( + BaseContainerRunner::ui_restrictions_bitmask( + &UiPolicy::default(), + &BaseProcessUiConfig::default() + ), + BaseContainerRunner::UILIMIT_GLOBALATOMS + ); + } + + #[test] + fn ui_bitmask_clipboard_read_with_default_backend() { + use crate::models::BaseProcessUiConfig; + let ui = UiPolicy { + disable: false, + clipboard: ClipboardPolicy::Read, + injection: true, + }; + let bp = BaseProcessUiConfig::default(); // isolation=container, desktopSystemControl=false, systemSettings=none, ime=false + let expected = BaseContainerRunner::UILIMIT_WRITECLIPBOARD + | BaseContainerRunner::UILIMIT_HANDLES + | BaseContainerRunner::UILIMIT_GLOBALATOMS + | BaseContainerRunner::UILIMIT_DESKTOP + | BaseContainerRunner::UILIMIT_EXITWINDOWS + | BaseContainerRunner::UILIMIT_SYSTEMPARAMETERS + | BaseContainerRunner::UILIMIT_DISPLAYSETTINGS + | BaseContainerRunner::UILIMIT_IME; + assert_eq!( + BaseContainerRunner::ui_restrictions_bitmask(&ui, &bp), + expected + ); + } + + #[test] + fn ui_bitmask_no_backend_restrictions() { + use crate::models::BaseProcessUiConfig; + let ui = UiPolicy { + disable: false, + clipboard: ClipboardPolicy::All, + injection: true, + }; + let bp = BaseProcessUiConfig { + isolation: "desktop".to_string(), + desktop_system_control: true, + system_settings: "all".to_string(), + ime: true, + }; + // No cross-platform restrictions + no backend restrictions = 0 + assert_eq!(BaseContainerRunner::ui_restrictions_bitmask(&ui, &bp), 0); + } } From 179aa8577537c3aa1ad74ea7f872ef23dd132fc4 Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Tue, 12 May 2026 12:46:37 -0700 Subject: [PATCH 3/5] update copilot instructions --- .github/copilot-instructions.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7ddf318c1..c90b9d084 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -107,7 +107,6 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the ` ### TypeScript layers - **SDK** (`sdk/`, `@microsoft/mxc-sdk`) — the public API. The one-shot surface (`spawnSandbox` / `spawnSandboxFromConfig` / `spawnSandboxAsync`) builds a `ContainerConfig` from a `SandboxPolicy`, serialises to base64, and spawns the correct native binary (`wxc-exec.exe` or `lxc-exec`) via `node-pty`. The state-aware surface (`provisionSandbox` / `startSandbox` / `execInSandbox` / `execInSandboxAsync` / `stopSandbox` / `deprovisionSandbox`, in `sdk/src/state-aware.ts`) drives a sandbox through a multi-call lifecycle against `StateAwareContainmentBackend` backends; per-(backend, phase) typed `*Config` interfaces and a branded `SandboxId` live in `sdk/src/state-aware-types.ts`. Typed wire-format errors live in `sdk/src/errors.ts` (closed `ErrorCode` union plus a single `MxcError` class carrying `code: ErrorCode`, mirroring the Rust `MxcError` shape). Platform detection is in `platform.ts`. -- **CLI** (`cli/`, `mxc-cli`) — thin Commander.js wrapper around the SDK. Depends on `@microsoft/mxc-sdk` via `file:../sdk`. The SDK auto-discovers native binaries by checking `sdk/bin//` (npm-packaged) and `src/target//{release,debug}/` (local dev). The `build.bat`/`build.sh` scripts copy binaries into the SDK bin directory. @@ -120,16 +119,32 @@ The SDK auto-discovers native binaries by checking `sdk/bin//` (n ### Key documentation (`docs/`) +Core references: + - `docs/schema.md` — full JSON configuration schema reference - `docs/versioning.md` — schema versioning design, experimental feature lifecycle, and promotion process - `docs/authoring-a-new-feature.md` — step-by-step guide for adding experimental features (which files to touch, in what order) -- `docs/lxc-backend.md` — LXC container backend details -- `docs/windows-sandbox.md` / `docs/windows-sandbox-reference.md` — Windows Sandbox backend +- `docs/examples.md` — annotated configuration examples (see also `examples/` and `test_configs/`) +- `docs/diagnostics.md` — diagnostic logging knobs (env vars, log file format) +- `docs/sandbox-policy/v1/policy.md` — sandbox policy v1 specification + +Per-backend guides: + +- `docs/base-process-container/guide.md` — process container (Windows AppContainer / BaseContainer) +- `docs/base-process-container/UIPolicy_Schema.md` — UI policy schema (JOB_OBJECT_UILIMIT_* mappings) +- `docs/lxc-support/lxc-backend.md` — LXC container backend (Linux) +- `docs/macos-support/seatbelt-backend.md` — macOS Seatbelt backend (experimental) +- `docs/windows-sandbox/windows-sandbox.md` / `docs/windows-sandbox/windows-sandbox-reference.md` — Windows Sandbox backend +- `docs/wsl/wsl-container-getting-started.md` / `docs/wsl/wsl-container-support-plan.md` — WSL Container (WSLC SDK) +- `docs/nanvix-microvm/nanvix.md` / `docs/nanvix-microvm/nanvix-integration-plan.md` — MicroVM via NanVix + +State-aware lifecycle: + - `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md` — state-aware sandbox lifecycle API (cross-backend wire format, Rust `StatefulSandboxBackend` trait, and dispatcher contract) - `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md` — companion overview to the full state-aware design - `docs/isolation-session/initial-bringup-plan.md` — IsolationSession backend, one-shot bringup (experimental, isolated user account per execution via the OS-side service) -- `docs/isolation-session/state-aware-rust-initial-plan.md` — IsolationSession state-aware lifecycle, Rust-layer initial plan (per-phase config / metadata, policy honor matrix, idempotence, concurrency, error mapping) -- `docs/examples.md` — annotated configuration examples (see also `examples/` and `test_configs/`) +- `docs/isolation-session/state-aware-rust-initial-plan.md` — IsolationSession state-aware lifecycle, Rust-layer plan (per-phase config / metadata, policy honor matrix, idempotence, concurrency, error mapping) +- `docs/isolation-session/state-aware-typescript-initial-plan.md` — IsolationSession state-aware lifecycle, TypeScript SDK plan ## Key Conventions @@ -174,7 +189,7 @@ When changing behavior covered by existing documentation, update the relevant do - **New experimental features** → follow `docs/authoring-a-new-feature.md`, which includes schema, Rust, and test config steps - **SDK API changes** (new exports, changed signatures, new options) → update `sdk/README.md` and the JSDoc in `sdk/src/index.ts` - **CLI command changes** → update `cli/README.md` and `cli/ARCHITECTURE.md` -- **New containment backends or major backend changes** → update the relevant doc in `docs/` (e.g., `lxc-backend.md`, `windows-sandbox.md`) +- **New containment backends or major backend changes** → update the relevant doc in `docs/` (e.g., `lxc-support/lxc-backend.md`, `windows-sandbox/windows-sandbox.md`) - **Versioning or promotion changes** → update `docs/versioning.md` ### Policy versioning From 81efb90e0dc722e2031ed75b6897bfd661b34780 Mon Sep 17 00:00:00 2001 From: Demitrius Nelon Date: Wed, 13 May 2026 09:47:05 -0700 Subject: [PATCH 4/5] Add macOS build section and Seatbelt backend to copilot instructions Addresses review feedback from richiemsft: adds build-mac.sh documentation, mxc-exec-mac binary reference, and Seatbelt backend entry to the containment backends table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c90b9d084..6659bf11d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,6 +28,17 @@ build.bat --with-microvm # Include NanVix micro-VM binaries ./build.sh --rust-only # Only Rust binaries, skip SDK/CLI ``` +### Full build (macOS) + +``` +./build-mac.sh # Release build for native architecture (seatbelt backend) +./build-mac.sh --debug # Debug build +./build-mac.sh --all # Build for both aarch64 and x86_64 +./build-mac.sh --rust-only # Only Rust binaries, skip SDK +``` + +Requires Xcode Command Line Tools and Rust. Produces an unsigned `mxc-exec-mac` binary (codesigning + notarization happen at release time). Schema `0.6.0-dev` or later required for macOS/Seatbelt backend. + ### Individual components ``` @@ -35,6 +46,7 @@ build.bat --with-microvm # Include NanVix micro-VM binaries cargo build --release --target x86_64-pc-windows-msvc cargo build --release --target aarch64-pc-windows-msvc cargo build --release -p lxc # Linux only — builds lxc-exec +cargo build --release -p mxc_darwin --target aarch64-apple-darwin # macOS only — builds mxc-exec-mac # SDK (from sdk/) npm install && npm run build @@ -95,8 +107,10 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the ` | BaseContainer (OS sandbox API) | `wxc-exec.exe` | Windows | `base_container_runner.rs` — calls `Experimental_CreateProcessInSandbox` via FlatBuffer | | Windows Sandbox | `wxc-exec.exe` | Windows | `windows_sandbox_runner.rs` | | MicroVM (NanVix) | `wxc-exec.exe` | Windows | `nanvix_runner.rs` — feature-gated behind `microvm` | +| Hyperlight | `wxc-exec.exe` | Windows | `hyperlight_runner.rs` — Hyperlight + Unikraft micro-VM backend | | IsolationSession | `wxc-exec.exe` | Windows | `isolation_session_runner.rs` — feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession` `IsoSessionOps` API (loaded from `IsoSessionApp.dll`). Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Honors `readwritePaths` and `readonlyPaths` at provision via `ShareFolderBatchAsync` (rejects `deniedPaths` since the API has no Deny ACE primitive); filesystem policy is immutable post-provision and rejected at later phases. State-aware additionally accepts an optional `user` bundle (`upn`, `wamToken`) at provision and start to provision Entra cloud-agent sandboxes; one-shot rejects the bundle, and hosts that don't support Entra agents surface `backend_unavailable`. Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. | | LXC | `lxc-exec` | Linux | `lxc/src/main.rs` + `lxc_common/` | +| Seatbelt | `mxc-exec-mac` | macOS | `mxc_darwin/src/main.rs` + `seatbelt_common/` — uses macOS App Sandbox (Seatbelt) profiles for process containment. Requires schema `0.6.0-dev`+. See `docs/macos-support/seatbelt-backend.md`. | ### Config flow @@ -106,9 +120,9 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the ` ### TypeScript layers -- **SDK** (`sdk/`, `@microsoft/mxc-sdk`) — the public API. The one-shot surface (`spawnSandbox` / `spawnSandboxFromConfig` / `spawnSandboxAsync`) builds a `ContainerConfig` from a `SandboxPolicy`, serialises to base64, and spawns the correct native binary (`wxc-exec.exe` or `lxc-exec`) via `node-pty`. The state-aware surface (`provisionSandbox` / `startSandbox` / `execInSandbox` / `execInSandboxAsync` / `stopSandbox` / `deprovisionSandbox`, in `sdk/src/state-aware.ts`) drives a sandbox through a multi-call lifecycle against `StateAwareContainmentBackend` backends; per-(backend, phase) typed `*Config` interfaces and a branded `SandboxId` live in `sdk/src/state-aware-types.ts`. Typed wire-format errors live in `sdk/src/errors.ts` (closed `ErrorCode` union plus a single `MxcError` class carrying `code: ErrorCode`, mirroring the Rust `MxcError` shape). Platform detection is in `platform.ts`. +- **SDK** (`sdk/`, `@microsoft/mxc-sdk`) — the public API. The one-shot surface (`spawnSandbox` / `spawnSandboxFromConfig` / `spawnSandboxAsync`) builds a `ContainerConfig` from a `SandboxPolicy`, serialises to base64, and spawns the correct native binary (`wxc-exec.exe`, `lxc-exec`, or `mxc-exec-mac`) via `node-pty`. The state-aware surface (`provisionSandbox` / `startSandbox` / `execInSandbox` / `execInSandboxAsync` / `stopSandbox` / `deprovisionSandbox`, in `sdk/src/state-aware.ts`) drives a sandbox through a multi-call lifecycle against `StateAwareContainmentBackend` backends; per-(backend, phase) typed `*Config` interfaces and a branded `SandboxId` live in `sdk/src/state-aware-types.ts`. Typed wire-format errors live in `sdk/src/errors.ts` (closed `ErrorCode` union plus a single `MxcError` class carrying `code: ErrorCode`, mirroring the Rust `MxcError` shape). Platform detection is in `platform.ts`. -The SDK auto-discovers native binaries by checking `sdk/bin//` (npm-packaged) and `src/target//{release,debug}/` (local dev). The `build.bat`/`build.sh` scripts copy binaries into the SDK bin directory. +The SDK auto-discovers native binaries by checking `sdk/bin//` (npm-packaged) and `src/target//{release,debug}/` (local dev). The `build.bat`/`build.sh`/`build-mac.sh` scripts copy binaries into the SDK bin directory. ### Schema system From 779ae43e958506ea080cff87c36337a9d09eded9 Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Wed, 13 May 2026 10:36:21 -0700 Subject: [PATCH 5/5] revert changes to base_container_runner --- src/wxc_common/src/base_container_runner.rs | 267 +++++--------------- 1 file changed, 68 insertions(+), 199 deletions(-) diff --git a/src/wxc_common/src/base_container_runner.rs b/src/wxc_common/src/base_container_runner.rs index 402de2367..d85c6bd78 100644 --- a/src/wxc_common/src/base_container_runner.rs +++ b/src/wxc_common/src/base_container_runner.rs @@ -24,8 +24,7 @@ use windows_core::PCWSTR; use crate::logger::Logger; use crate::models::{ - BaseProcessUiConfig, ClipboardPolicy, CodexRequest, NetworkEnforcementMode, NetworkPolicy, - ProxyAddress, ScriptResponse, UiPolicy, + CodexRequest, NetworkEnforcementMode, NetworkPolicy, ProxyAddress, ScriptResponse, }; use crate::proxy_coordinator::ProxyCoordinator; use crate::script_runner::{get_timeout_milliseconds, ScriptRunner}; @@ -84,95 +83,6 @@ impl BaseContainerRunner { Self::load_api().map(|_| ()) } - /// JOB_OBJECT_UILIMIT_* flag constants (from base-process-container/UIPolicy_Schema.md). - const UILIMIT_HANDLES: u64 = 0x0001; - const UILIMIT_READCLIPBOARD: u64 = 0x0002; - const UILIMIT_WRITECLIPBOARD: u64 = 0x0004; - const UILIMIT_SYSTEMPARAMETERS: u64 = 0x0008; - const UILIMIT_DISPLAYSETTINGS: u64 = 0x0010; - const UILIMIT_GLOBALATOMS: u64 = 0x0020; - const UILIMIT_DESKTOP: u64 = 0x0040; - const UILIMIT_EXITWINDOWS: u64 = 0x0080; - const UILIMIT_IME: u64 = 0x0100; - const UILIMIT_INJECTION: u64 = 0x0200; - - /// Build the JOB_OBJECT_UILIMIT_* bitmask from the cross-platform UI policy - /// and the BaseProcessContainer-specific UI config. - /// Mapping follows docs/base-process-container/UIPolicy_Schema.md. - fn ui_restrictions_bitmask(ui: &UiPolicy, base_proc_ui: &BaseProcessUiConfig) -> u64 { - // When UI is fully disabled: DisallowWin32kSystemCalls handles everything - // except atoms (NT executive syscalls, not Win32k). Only set GLOBALATOMS. - if ui.disable { - return Self::UILIMIT_GLOBALATOMS; - } - - let mut mask: u64 = 0; - - // Cross-platform: clipboard (default: "none" = block both) - match ui.clipboard { - ClipboardPolicy::All => {} - ClipboardPolicy::Read => { - mask |= Self::UILIMIT_WRITECLIPBOARD; - } - ClipboardPolicy::Write => { - mask |= Self::UILIMIT_READCLIPBOARD; - } - // "none" or unrecognized → default-deny: block both - _ => { - mask |= Self::UILIMIT_READCLIPBOARD | Self::UILIMIT_WRITECLIPBOARD; - } - } - - // Cross-platform: input injection - if !ui.injection { - mask |= Self::UILIMIT_INJECTION; - } - - // Backend-specific: isolation level (default: "container" = HANDLES + GLOBALATOMS) - match base_proc_ui.isolation.as_str() { - "desktop" => { - // No isolation flags - } - "handles" => { - mask |= Self::UILIMIT_HANDLES; - } - "atoms" => { - mask |= Self::UILIMIT_GLOBALATOMS; - } - // "container" or unrecognized → default-deny: full isolation - _ => { - mask |= Self::UILIMIT_HANDLES | Self::UILIMIT_GLOBALATOMS; - } - } - - // Backend-specific: desktop system control - if !base_proc_ui.desktop_system_control { - mask |= Self::UILIMIT_DESKTOP | Self::UILIMIT_EXITWINDOWS; - } - - // Backend-specific: system settings (default: "none" = block all) - match base_proc_ui.system_settings.as_str() { - "all" => {} - "parameters" => { - mask |= Self::UILIMIT_DISPLAYSETTINGS; - } - "display" => { - mask |= Self::UILIMIT_SYSTEMPARAMETERS; - } - // "none" or unrecognized → default-deny: block all - _ => { - mask |= Self::UILIMIT_SYSTEMPARAMETERS | Self::UILIMIT_DISPLAYSETTINGS; - } - } - - // Backend-specific: IME - if !base_proc_ui.ime { - mask |= Self::UILIMIT_IME; - } - - mask - } - /// Build a FlatBuffer `SandboxSpec` from the container policy in the request. /// /// Maps `ContainerPolicy` and `UiPolicy` fields to the BaseContainer schema: @@ -264,8 +174,12 @@ impl BaseContainerRunner { }; // UI restrictions - let ui_restrictions = - Self::ui_restrictions_bitmask(&request.policy.ui, &request.policy.base_process_ui); + let ui_restrictions = crate::job_object::to_job_object_uilimit_mask( + &crate::ui_policy::resolve_ui_restrictions( + &request.policy.ui, + &request.policy.base_process_ui, + ), + ) as u64; let spec = SandboxSpec::create( &mut builder, @@ -392,8 +306,11 @@ impl ScriptRunner for BaseContainerRunner { let _ = writeln!(logger, "proxy URL in spec: {}", addr.to_url()); } - let ui_restrictions = - Self::ui_restrictions_bitmask(&request.policy.ui, &request.policy.base_process_ui); + let restrictions = crate::ui_policy::resolve_ui_restrictions( + &request.policy.ui, + &request.policy.base_process_ui, + ); + let ui_restrictions = crate::job_object::to_job_object_uilimit_mask(&restrictions); let _ = writeln!( logger, "sandbox spec built (version={}, {} bytes)", @@ -423,16 +340,16 @@ impl ScriptRunner for BaseContainerRunner { let _ = writeln!( logger, "UILIMIT flags: HANDLES={} READCLIP={} WRITECLIP={} SYSPARAM={} DISPLAY={} ATOMS={} DESKTOP={} EXIT={} IME={} INJECT={}", - ui_restrictions & Self::UILIMIT_HANDLES != 0, - ui_restrictions & Self::UILIMIT_READCLIPBOARD != 0, - ui_restrictions & Self::UILIMIT_WRITECLIPBOARD != 0, - ui_restrictions & Self::UILIMIT_SYSTEMPARAMETERS != 0, - ui_restrictions & Self::UILIMIT_DISPLAYSETTINGS != 0, - ui_restrictions & Self::UILIMIT_GLOBALATOMS != 0, - ui_restrictions & Self::UILIMIT_DESKTOP != 0, - ui_restrictions & Self::UILIMIT_EXITWINDOWS != 0, - ui_restrictions & Self::UILIMIT_IME != 0, - ui_restrictions & Self::UILIMIT_INJECTION != 0, + restrictions.block_external_ui_objects, + restrictions.block_clipboard_read, + restrictions.block_clipboard_write, + restrictions.block_system_parameter_changes, + restrictions.block_display_settings_changes, + restrictions.block_global_ui_namespace, + restrictions.block_desktop_switching, + restrictions.block_logoff_or_shutdown, + restrictions.block_input_method_changes, + restrictions.block_input_injection, ); let _ = writeln!(logger, "SECTION: Load API"); @@ -572,9 +489,15 @@ impl ScriptRunner for BaseContainerRunner { #[cfg(test)] mod tests { use super::*; - use crate::models::ProxyConfig; + use crate::job_object::to_job_object_uilimit_mask; + use crate::models::{ClipboardPolicy, ProxyConfig, UiPolicy}; + use crate::ui_policy::EffectiveUiRestrictions; use sandbox_spec::base_container_layout; + fn expected_mask(r: EffectiveUiRestrictions) -> u64 { + to_job_object_uilimit_mask(&r) as u64 + } + #[test] fn build_sandbox_spec_produces_valid_flatbuffer() { let mut request = CodexRequest::default(); @@ -598,10 +521,14 @@ mod tests { assert!(spec.least_privilege()); assert_eq!(spec.capabilities(), Some("internetClient,registryRead")); assert!(spec.disallow_win32k_system_calls()); + // default: disable=true → only the global UI namespace bit assert_eq!( spec.ui_restrictions(), - BaseContainerRunner::UILIMIT_GLOBALATOMS - ); // default: disable=true → only GLOBALATOMS + expected_mask(EffectiveUiRestrictions { + block_global_ui_namespace: true, + ..Default::default() + }) + ); let rw = spec.fs_read_write().unwrap(); assert_eq!(rw.len(), 1); @@ -648,8 +575,6 @@ mod tests { #[test] fn build_sandbox_spec_ui_disabled() { - use crate::models::UiPolicy; - let mut request = CodexRequest::default(); request.policy.ui = UiPolicy { disable: true, @@ -660,10 +585,13 @@ mod tests { let spec = base_container_layout::root_as_sandbox_spec(&bytes).unwrap(); assert!(spec.disallow_win32k_system_calls()); - // disable=true → only GLOBALATOMS (Win32k disable handles the rest) + // disable=true → only the global UI namespace bit (Win32k disable handles the rest) assert_eq!( spec.ui_restrictions(), - BaseContainerRunner::UILIMIT_GLOBALATOMS + expected_mask(EffectiveUiRestrictions { + block_global_ui_namespace: true, + ..Default::default() + }) ); } @@ -682,15 +610,20 @@ mod tests { assert!(!spec.disallow_win32k_system_calls()); // WRITECLIPBOARD + backend defaults (isolation=container: HANDLES+GLOBALATOMS, // desktopSystemControl=false: DESKTOP+EXITWINDOWS, systemSettings=none: SYSTEMPARAMETERS+DISPLAYSETTINGS, ime=false: IME) - let expected = BaseContainerRunner::UILIMIT_WRITECLIPBOARD - | BaseContainerRunner::UILIMIT_HANDLES - | BaseContainerRunner::UILIMIT_GLOBALATOMS - | BaseContainerRunner::UILIMIT_DESKTOP - | BaseContainerRunner::UILIMIT_EXITWINDOWS - | BaseContainerRunner::UILIMIT_SYSTEMPARAMETERS - | BaseContainerRunner::UILIMIT_DISPLAYSETTINGS - | BaseContainerRunner::UILIMIT_IME; - assert_eq!(spec.ui_restrictions(), expected); + assert_eq!( + spec.ui_restrictions(), + expected_mask(EffectiveUiRestrictions { + block_clipboard_write: true, + block_external_ui_objects: true, + block_global_ui_namespace: true, + block_desktop_switching: true, + block_logoff_or_shutdown: true, + block_system_parameter_changes: true, + block_display_settings_changes: true, + block_input_method_changes: true, + ..Default::default() + }) + ); } #[test] @@ -707,15 +640,20 @@ mod tests { assert!(!spec.disallow_win32k_system_calls()); // INJECTION + backend defaults - let expected = BaseContainerRunner::UILIMIT_INJECTION - | BaseContainerRunner::UILIMIT_HANDLES - | BaseContainerRunner::UILIMIT_GLOBALATOMS - | BaseContainerRunner::UILIMIT_DESKTOP - | BaseContainerRunner::UILIMIT_EXITWINDOWS - | BaseContainerRunner::UILIMIT_SYSTEMPARAMETERS - | BaseContainerRunner::UILIMIT_DISPLAYSETTINGS - | BaseContainerRunner::UILIMIT_IME; - assert_eq!(spec.ui_restrictions(), expected); + assert_eq!( + spec.ui_restrictions(), + expected_mask(EffectiveUiRestrictions { + block_input_injection: true, + block_external_ui_objects: true, + block_global_ui_namespace: true, + block_desktop_switching: true, + block_logoff_or_shutdown: true, + block_system_parameter_changes: true, + block_display_settings_changes: true, + block_input_method_changes: true, + ..Default::default() + }) + ); } #[test] @@ -744,73 +682,4 @@ mod tests { let spec = base_container_layout::root_as_sandbox_spec(&bytes).unwrap(); assert!(spec.network_policy().is_none()); } - - #[test] - fn ui_bitmask_disabled() { - use crate::models::BaseProcessUiConfig; - let ui = UiPolicy { - disable: true, - ..Default::default() - }; - let bp = BaseProcessUiConfig::default(); - // disable=true → only GLOBALATOMS - assert_eq!( - BaseContainerRunner::ui_restrictions_bitmask(&ui, &bp), - BaseContainerRunner::UILIMIT_GLOBALATOMS - ); - } - - #[test] - fn ui_bitmask_default_deny() { - use crate::models::BaseProcessUiConfig; - // UiPolicy default: disable=true → only GLOBALATOMS - assert_eq!( - BaseContainerRunner::ui_restrictions_bitmask( - &UiPolicy::default(), - &BaseProcessUiConfig::default() - ), - BaseContainerRunner::UILIMIT_GLOBALATOMS - ); - } - - #[test] - fn ui_bitmask_clipboard_read_with_default_backend() { - use crate::models::BaseProcessUiConfig; - let ui = UiPolicy { - disable: false, - clipboard: ClipboardPolicy::Read, - injection: true, - }; - let bp = BaseProcessUiConfig::default(); // isolation=container, desktopSystemControl=false, systemSettings=none, ime=false - let expected = BaseContainerRunner::UILIMIT_WRITECLIPBOARD - | BaseContainerRunner::UILIMIT_HANDLES - | BaseContainerRunner::UILIMIT_GLOBALATOMS - | BaseContainerRunner::UILIMIT_DESKTOP - | BaseContainerRunner::UILIMIT_EXITWINDOWS - | BaseContainerRunner::UILIMIT_SYSTEMPARAMETERS - | BaseContainerRunner::UILIMIT_DISPLAYSETTINGS - | BaseContainerRunner::UILIMIT_IME; - assert_eq!( - BaseContainerRunner::ui_restrictions_bitmask(&ui, &bp), - expected - ); - } - - #[test] - fn ui_bitmask_no_backend_restrictions() { - use crate::models::BaseProcessUiConfig; - let ui = UiPolicy { - disable: false, - clipboard: ClipboardPolicy::All, - injection: true, - }; - let bp = BaseProcessUiConfig { - isolation: "desktop".to_string(), - desktop_system_control: true, - system_settings: "all".to_string(), - ime: true, - }; - // No cross-platform restrictions + no backend restrictions = 0 - assert_eq!(BaseContainerRunner::ui_restrictions_bitmask(&ui, &bp), 0); - } }