Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/workflows/hyperlight-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Hyperlight E2E Tests

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
hyperlight-e2e:
name: WXC-Exec Hyperlight
runs-on: windows-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v6

- name: Setup Rust toolchain
run: |
rustup update stable
rustup target add x86_64-pc-windows-msvc

- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: src

- name: Ensure surrogate build consistency
shell: pwsh
run: |
$hlsExe = "src\target\x86_64-pc-windows-msvc\debug\hls\x86_64-pc-windows-msvc\debug\hyperlight_surrogate.exe"
if (-not (Test-Path $hlsExe)) {
Write-Host "Surrogate missing — clearing hyperlight-host fingerprints to force rebuild"
Get-ChildItem "src\target\x86_64-pc-windows-msvc\debug\.fingerprint" -Filter "hyperlight-host-*" -Directory -ErrorAction SilentlyContinue |
Remove-Item -Recurse -Force
}

- name: Build with Hyperlight support
working-directory: src
run: cargo build --features hyperlight --target x86_64-pc-windows-msvc

- name: Diagnose hypervisor environment
shell: pwsh
run: ./scripts/ci/diagnose-whp.ps1

- name: Check Windows Hypervisor Platform
id: whp-check
shell: pwsh
run: ./scripts/ci/check-whp.ps1

- name: Install crane (OCI tool)
if: steps.whp-check.outputs.whp_available == 'true'
shell: pwsh
run: |
$url = "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Windows_x86_64.tar.gz"
Invoke-WebRequest -Uri $url -OutFile crane.tar.gz -UseBasicParsing
tar -xzf crane.tar.gz crane.exe

- name: Download Hyperlight kernel and initrd
if: steps.whp-check.outputs.whp_available == 'true'
shell: pwsh
run: |
$pyhlHome = Join-Path $env:LOCALAPPDATA "pyhl"
New-Item -ItemType Directory -Force -Path $pyhlHome | Out-Null

.\crane.exe export ghcr.io/danbugs/hyperlight-unikraft/python-agent-driver-kernel:latest kernel.tar
tar -xf kernel.tar -C $pyhlHome kernel

.\crane.exe export ghcr.io/danbugs/hyperlight-unikraft/python-agent-driver-initrd:latest initrd.tar
tar -xf initrd.tar -C $pyhlHome initrd.cpio

Write-Host "Downloaded to ${pyhlHome}:"
Get-ChildItem $pyhlHome

- name: Warm Hyperlight snapshot
if: steps.whp-check.outputs.whp_available == 'true'
shell: pwsh
run: |
$binDir = Join-Path $env:GITHUB_WORKSPACE "src\target\x86_64-pc-windows-msvc\debug"
$json = '{"process":{"commandLine":"print(\"snapshot warm\")"},"containment":"hyperlight"}'
$b64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($json))
& "$binDir\wxc-exec.exe" --experimental --config-base64 $b64 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "::error::Hyperlight snapshot warmup failed"
exit 1
}
Write-Host "Snapshot created:"
Get-ChildItem (Join-Path $env:LOCALAPPDATA "pyhl")

- name: Run Hyperlight E2E Tests
if: steps.whp-check.outputs.whp_available == 'true'
shell: pwsh
working-directory: src
run: |
cargo test -p wxc_e2e_tests --target x86_64-pc-windows-msvc test_hyperlight_suite -- --nocapture

- name: Upload logs on failure
if: failure() || cancelled()
uses: actions/upload-artifact@v6
with:
name: hyperlight-e2e-logs-${{ github.event.pull_request.number || github.run_number }}
retention-days: 7
path: |
logs/
**/*.log
4 changes: 4 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ set "BUILD_ALL=0"
set "WITH_NANVIX=0"
set "WITH_WSLC=0"
set "WITH_ISOLATION_SESSION=0"
set "WITH_HYPERLIGHT=0"

:: Parse arguments
:parse_args
Expand All @@ -20,6 +21,7 @@ if /i "%~1"=="--all" ( set "BUILD_ALL=1" & shift & goto :parse_arg
if /i "%~1"=="--with-microvm" ( set "WITH_NANVIX=1" & shift & goto :parse_args )
if /i "%~1"=="--with-wslc" ( set "WITH_WSLC=1" & shift & goto :parse_args )
if /i "%~1"=="--with-isolation-session" ( set "WITH_ISOLATION_SESSION=1" & shift & goto :parse_args )
if /i "%~1"=="--with-hyperlight" ( set "WITH_HYPERLIGHT=1" & shift & goto :parse_args )
if /i "%~1"=="--help" ( goto :usage )
if /i "%~1"=="-h" ( goto :usage )
echo Unknown argument: %~1
Expand All @@ -41,6 +43,7 @@ if "%BUILD_CONFIG%"=="release" set "CARGO_FLAGS=--release --target"
if "%WITH_NANVIX%"=="1" set "CARGO_FLAGS=--features microvm %CARGO_FLAGS%"
if "%WITH_WSLC%"=="1" set "CARGO_FLAGS=--features wslc %CARGO_FLAGS%"
if "%WITH_ISOLATION_SESSION%"=="1" set "CARGO_FLAGS=--features isolation_session %CARGO_FLAGS%"
if "%WITH_HYPERLIGHT%"=="1" set "CARGO_FLAGS=--features hyperlight %CARGO_FLAGS%"

:: Build Rust
echo.
Expand Down Expand Up @@ -140,6 +143,7 @@ echo --all Build for both x64 and ARM64
echo --with-microvm Download and include NanVix micro-VM binaries
echo --with-wslc Build with WSL Container (WSLC SDK) support
echo --with-isolation-session Build with IsolationSession backend (IsoEnvBroker)
echo --with-hyperlight Build with Hyperlight (micro-VM) backend (x86_64 only)
echo -h, --help Show this help
echo.
echo Default: builds release for the current machine architecture.
Expand Down
22 changes: 17 additions & 5 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ CLI_DIR="$SCRIPT_DIR/cli"
BUILD_TYPE="release"
BUILD_SDK=true

WITH_HYPERLIGHT=false

while [[ $# -gt 0 ]]; do
case $1 in
--debug)
Expand All @@ -23,13 +25,18 @@ while [[ $# -gt 0 ]]; do
BUILD_SDK=false
shift
;;
--with-hyperlight)
WITH_HYPERLIGHT=true
shift
;;
--help|-h)
echo "Usage: build.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --debug Build in debug mode (default: release)"
echo " --rust-only Only build Rust binaries, skip SDK/CLI"
echo " -h, --help Show this help message"
echo " --debug Build in debug mode (default: release)"
echo " --rust-only Only build Rust binaries, skip SDK/CLI"
echo " --with-hyperlight Build with Hyperlight (micro-VM) backend (x86_64 only)"
echo " -h, --help Show this help message"
exit 0
;;
*)
Expand Down Expand Up @@ -57,10 +64,15 @@ echo ""
echo "=== Building Rust binaries ($BUILD_TYPE) ==="
cd "$SRC_DIR"

CARGO_FEATURES=""
if [ "$WITH_HYPERLIGHT" = true ]; then
CARGO_FEATURES="--features hyperlight"
fi

if [ "$BUILD_TYPE" = "release" ]; then
cargo build --release -p lxc
cargo build --release -p lxc $CARGO_FEATURES
else
cargo build -p lxc
cargo build -p lxc $CARGO_FEATURES
fi

echo "Rust build complete."
Expand Down
191 changes: 191 additions & 0 deletions docs/hyperlight-integration-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# MXC Hyperlight Integration — Design Document

## Problem

MXC needs a **cross-platform micro-VM execution backend** with a good story
Comment thread
MGudgin marked this conversation as resolved.
for agentic Python workloads that care about cold-start time. The backend
should work identically on Linux and Windows, boot in milliseconds, and
provide hardware-level isolation.

## Proposed Solution

Add a **Hyperlight backend** — embedded [Hyperlight](https://github.com/hyperlight-dev/hyperlight)
+ [Unikraft](https://unikraft.org/) driving a warmed-up CPython snapshot
via the [`hyperlight-unikraft-host`](https://github.com/hyperlight-dev/hyperlight-unikraft) library.

When the JSON config specifies `"containment": "hyperlight"`, `wxc-exec`
routes to `HyperlightScriptRunner`, which instantiates a Hyperlight micro-VM
directly in-process. Every `run_code(&script)` rewinds to the snapshot and runs
hermetic.

**Cross-platform:** KVM on Linux, WHP on Windows — same code path, same
library.

## Performance

> Benchmarks: bare-metal Windows (Hyper-V / WHP).
> pyhl 0.1.0 (the CLI from `hyperlight-unikraft-host` for running python-agent unikernels), CPython 3.12.0, x86_64. 15 runs.

| Metric | Median | Avg | Min | Max |
|--------|--------|-----|-----|-----|
| Hello world (`print(42)`, end-to-end) | 139 ms | 141 ms | 133 ms | 157 ms |

## Density

| Metric | Value |
|--------|-------|
| Per-VM memory | 17 MB |
| Shared snapshot (one-time, mapped read-only CoW) | ~650 MiB on disk (2 GiB apparent) |

The snapshot file is 2 GiB in apparent size but only ~650 MiB on disk
thanks to sparse-file hole-punching (`fallocate(PUNCH_HOLE)` on Linux,
`FSCTL_SET_SPARSE` on Windows). It is mmap'd read-only and shared across
all VMs — each new VM only pays for pages it actually writes.

## Ecosystem

**Hyperlight-Unikraft** builds on two open-source foundations:

- **[Unikraft](https://unikraft.org/)** — Linux Foundation project with
an active community, regular releases, and commercial backing. Hyperlight
platform support has been upstreamed
([unikraft/unikraft#1821](https://github.com/unikraft/unikraft/pull/1821),
[unikraft/app-elfloader#102](https://github.com/unikraft/app-elfloader/pull/102),
[unikraft/kraftkit#2797](https://github.com/unikraft/kraftkit/pull/2797)).
- **[Hyperlight](https://github.com/hyperlight-dev/hyperlight)** — CNCF
sandbox project. Already adopted across multiple Microsoft organizations
including Edge Actions, HorizonDB, and the Agentic Framework.

Beyond Python, hyperlight-unikraft supports .NET, Node.js, Go, Rust,
C/C++, PowerShell, and Bash/Shell runtimes.

## Why a separate `Hyperlight` variant

- **Non-breaking.** Existing containment backends are unaffected.
- **Distinct semantics.** Hyperlight has:
- A pre-installed warm snapshot as a prerequisite (not just binaries).
- An in-process execution model.
- A rich stdlib (full CPython + ~20 pre-imported packages including C
extensions: numpy, pandas, Pillow, pydantic, cryptography, lxml).
- Live VFS forwarding for host filesystem access — guest POSIX calls
are forwarded to the host in real-time, limited only by host disk.
- **Different artifact provenance.** Hyperlight images come from
`hyperlight-dev/hyperlight-unikraft`'s `python-agent-driver` pipeline.
Adding new packages is a Dockerfile change + rebuild.

## Design Decisions

1. **In-process, not subprocess.** Hyperlight is a Rust library; wxc-exec
is a Rust binary. Linking directly avoids pipe plumbing, watchdog
threads, and process lifecycle management.

2. **`script_code` is raw Python source.** No shell quoting, no cmdline
limit (`run_code` takes `&str` unbounded).

3. **`--experimental` gate.** Keeps this backend off the happy path until
artifact distribution and docs catch up.

4. **Unsupported policies are rejected.** A config specifying `network`
or `workingDirectory` with `containment: "hyperlight"` produces a preflight
error.

5. **Image artifacts are user-provided, not bundled.** The `--setup-hyperlight`
flag populates the image home. The runner auto-discovers
`$PYHL_HOME` → `<exe>/pyhl/` → `<cwd>/.pyhl/`; the first location
with all three files wins.

6. **Exit codes.** 0 on clean completion of `run_code`; -1 on any error
(preflight, runtime, guest crash). Distinct per-error variants go
through `error_message`.

7. **stdout/stderr are inherited.** Guest `print(...)` reaches the user's
terminal directly via Hyperlight's `host_print`.
`ScriptResponse.standard_{out,err}` stay empty — consumers who need
capture redirect wxc-exec at the process level.

## Workspace Changes

```
mxc/src/wxc_common/
├── Cargo.toml # + hyperlight-unikraft-host dependency
└── src/
├── lib.rs # + pub mod hyperlight_runner;
├── models.rs # + ContainmentBackend::Hyperlight (serde "hyperlight")
├── config_parser.rs # + Some("hyperlight") => Hyperlight match arm
└── hyperlight_runner.rs # NEW

mxc/src/wxc/
└── src/main.rs # + ContainmentBackend::Hyperlight dispatch arm

mxc/test_configs/
├── hyperlight_hello.json # NEW — hello from Python
└── hyperlight_pandas.json # NEW — exercises pre-imported numpy/pandas

mxc/docs/
└── hyperlight-integration-plan.md # NEW — this document
```

## Configuration

### JSON

```json
{
"process": {
"commandLine": "import sys\nprint(f'Python {sys.version.split()[0]} on {sys.platform}')",
"timeout": 30000
},
"containment": "hyperlight"
}
```

### Field semantics

| JSON Field | Hyperlight Behavior |
|------------|---------------|
| `process.commandLine` | ✅ Used — raw Python source |
| `process.timeout` | ✅ Used — script execution timeout (ms) |
| `containment` | ✅ Must be `"hyperlight"` |
| `filesystem.*` | ✅ `readwritePaths`/`readonlyPaths` mapped to host mounts |
| `network.*` | ❌ Rejected |
| `workingDirectory` | ❌ Rejected (guest has its own FS namespace) |
| `appContainer.*`, `sandbox.*` | ❌ N/A |

## Security Model

| Property | Hyperlight |
|----------|------|
| Isolation level | Micro VM (KVM/WHP) |
| Host FS access | Explicit mounts via `Preopen` |
| Network | None |
| Guest OS | Unikraft unikernel |
| Cold start | ~30ms KVM / ~140 ms WHP |
| Host platforms | Linux + Windows |

## Supported Workloads

### Supported out of the box (preloaded in snapshot)

| Category | Examples |
|----------|----------|
| Stdlib | `os`, `sys`, `json`, `re`, `pathlib`, `datetime`, `hashlib`, `itertools`, `functools`, `math`, `decimal`, `fractions`, `collections`, `statistics` |
| Pre-imported 3rd-party | `numpy`, `pandas`, `pydantic`, `yaml`, `jinja2`, `bs4`, `tabulate`, `click`, `tenacity`, `tqdm`, `openpyxl`, `pypdf`, `markdown_it`, `PIL`, `lxml`, `cryptography`, `dateutil`, `dotenv` |

### Not supported

| Why not | Example failure |
|---------|-----------------|
| No network stack in guest | `urllib`, `socket`, `http` — `OSError: Function not implemented` |
| Read-only sysroot by default | File writes under `/` — `OSError: Read-only file system` |
| No subprocess / fork | `subprocess.run` — `OSError: Function not implemented` |

## Testing Strategy

### Unit tests (`cargo test -p wxc_common`)

- `is_installed_false_on_empty_dir` — negative case for the install probe
- `resolve_home_errors_when_nothing_configured` — actionable error when no image
- `policy_rejects_filesystem_paths` — blocks readwritePaths/readonlyPaths/deniedPaths
- `policy_rejects_network_rules` — blocks allowed/blockedHosts
- `policy_rejects_block_default_network` — blocks `defaultNetworkPolicy: block`
- `policy_rejects_working_directory` — blocks non-empty `workingDirectory`
Loading
Loading