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
8 changes: 8 additions & 0 deletions .github/workflows/host-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
- name: Cargo clippy (warnings are errors)
run: cargo clippy --all-targets --locked -- -D warnings

- name: Cargo clippy with Wasm host tools
run: cargo clippy --bin hyperlight-unikraft --tests --locked --features wasm-host-fns -- -D warnings

- name: Enable KVM permissions
working-directory: .
run: |
Expand All @@ -75,6 +78,11 @@ jobs:
RUST_BACKTRACE: '1'
run: cargo test --all-targets --locked

- name: Cargo test with Wasm host tools
env:
RUST_BACKTRACE: '1'
run: cargo test --bin hyperlight-unikraft --locked --features wasm-host-fns wasm_host_fns::tests

host-checks-passed:
if: always()
needs: [checks]
Expand Down
74 changes: 54 additions & 20 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ on:
push:
branches: [main]
paths:
- 'examples/**'
- 'runtimes/**'
- 'host/**'
- '.github/workflows/test-examples.yml'
- "examples/**"
- "runtimes/**"
- "host/**"
- ".github/workflows/test-examples.yml"

# Pushing a new commit to the same ref cancels any in-flight run on
# the previous commit — don't waste CI on stale code.
Expand Down Expand Up @@ -68,7 +68,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.25.1'
go-version: "1.25.1"
cache: false

- name: Install just
Expand All @@ -92,7 +92,7 @@ jobs:
- name: Build local-python-base images (python-agent-driver only)
if: matrix.example == 'python-agent-driver'
env:
DOCKER_BUILDKIT: '0'
DOCKER_BUILDKIT: "0"
run: |
docker build --target base -t local-python-base-dev:latest \
-f runtimes/python.Dockerfile runtimes/
Expand All @@ -102,7 +102,7 @@ jobs:
- name: Build rootfs
working-directory: examples/${{ matrix.example }}
env:
DOCKER_BUILDKIT: '0'
DOCKER_BUILDKIT: "0"
run: |
just rootfs

Expand Down Expand Up @@ -185,8 +185,9 @@ jobs:
args: "-- /hello.py"
expect: "Hello from Python on Hyperlight"
- example: python-tools
args: "--enable-tools -- /test_tools.py"
args: "-- /test_tools.py"
expect: "Tool returned"
needs_echo_tool: true
- example: nodejs
args: "-- /app/hello.js"
expect: "Hello from Node.js on Hyperlight"
Expand Down Expand Up @@ -280,13 +281,19 @@ jobs:
if: steps.kvm_check.outputs.available == 'true'
run: |
cd host
cargo build --release --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run
cargo build --release --features wasm-host-fns --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run
sudo cp target/release/hyperlight-unikraft /usr/local/bin/

- name: Build echo Wasm host function
if: steps.kvm_check.outputs.available == 'true' && matrix.needs_echo_tool == true
run: |
rustup target add wasm32-wasip1
cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1

- name: Build local-python-base images (python-agent-driver only)
if: steps.kvm_check.outputs.available == 'true' && matrix.example == 'python-agent-driver'
env:
DOCKER_BUILDKIT: '0'
DOCKER_BUILDKIT: "0"
run: |
docker build --target base -t local-python-base-dev:latest \
-f runtimes/python.Dockerfile runtimes/
Expand All @@ -297,7 +304,7 @@ jobs:
if: steps.kvm_check.outputs.available == 'true'
working-directory: examples/${{ matrix.example }}
env:
DOCKER_BUILDKIT: '0'
DOCKER_BUILDKIT: "0"
run: |
just rootfs
kraft-hyperlight --no-prompt build --plat hyperlight --arch x86_64 || true
Expand Down Expand Up @@ -348,14 +355,20 @@ jobs:
mkdir -p "$mount_dir"
mount_args="--mount $mount_dir:/host"
fi

tool_args=""
if [ "${{ matrix.needs_echo_tool }}" = "true" ]; then
tool_args="--tool echo=../echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm"
fi

# HTTP server examples: start in background, poll, curl, kill.
http_port="${{ matrix.http_port }}"
if [ -n "$http_port" ]; then
mem_args=""
if [ -n "$memory" ]; then
mem_args="-m $memory"
fi
hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" ${{ matrix.args }} &
hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args $tool_args ${{ matrix.args }} &
server_pid=$!
sleep 3
ready=0
Expand Down Expand Up @@ -396,7 +409,7 @@ jobs:
if [ -n "$memory" ]; then
mem_args="-m $memory"
fi
cmd=(timeout 120 hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args ${{ matrix.args }})
cmd=(timeout 120 hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args $tool_args ${{ matrix.args }})
;;
esac
set +e
Expand Down Expand Up @@ -456,7 +469,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.25.1'
go-version: "1.25.1"
cache: false

- name: Install just
Expand All @@ -480,7 +493,7 @@ jobs:
- name: Build local-python-base images (python-agent-driver only)
if: matrix.example == 'python-agent-driver'
env:
DOCKER_BUILDKIT: '0'
DOCKER_BUILDKIT: "0"
run: |
docker build --target base -t local-python-base-dev:latest \
-f runtimes/python.Dockerfile runtimes/
Expand All @@ -490,7 +503,7 @@ jobs:
- name: Build rootfs + kernel
working-directory: examples/${{ matrix.example }}
env:
DOCKER_BUILDKIT: '0'
DOCKER_BUILDKIT: "0"
run: |
just rootfs
kraft-hyperlight --no-prompt build --plat hyperlight --arch x86_64 || true
Expand Down Expand Up @@ -562,8 +575,9 @@ jobs:
args: "-- /hello.py"
expect: "Hello from Python on Hyperlight"
- example: python-tools
args: "--enable-tools -- /test_tools.py"
args: "-- /test_tools.py"
expect: "Tool returned"
needs_echo_tool: true
- example: nodejs
args: "-- /app/hello.js"
expect: "Hello from Node.js on Hyperlight"
Expand Down Expand Up @@ -631,12 +645,19 @@ jobs:
shell: pwsh
run: |
cd host
cargo build --release --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run --bin pyhl
cargo build --release --features wasm-host-fns --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run --bin pyhl
Copy-Item target\release\hyperlight-unikraft.exe $env:USERPROFILE\.cargo\bin\ -Force
Copy-Item target\release\multifn-test.exe $env:USERPROFILE\.cargo\bin\ -Force
Copy-Item target\release\pydriver-run.exe $env:USERPROFILE\.cargo\bin\ -Force
Copy-Item target\release\pyhl.exe $env:USERPROFILE\.cargo\bin\ -Force

- name: Build echo Wasm host function
if: matrix.needs_echo_tool == true
shell: pwsh
run: |
rustup target add wasm32-wasip1
cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1

- name: Download prebuilt image
uses: actions/download-artifact@v4
with:
Expand All @@ -652,6 +673,7 @@ jobs:
$driver = '${{ matrix.driver }}'
$runArgs = '${{ matrix.args }}'
$needsMount = '${{ matrix.needs_mount }}'
$needsEchoTool = '${{ matrix.needs_echo_tool }}'

# Read memory from the example's Justfile (single source of truth).
$memory = ''
Expand All @@ -673,6 +695,11 @@ jobs:
$mountArgs = @('--mount', ($mountDir + ':/host'))
}

$toolArgs = @()
if ($needsEchoTool -eq 'true') {
$toolArgs = @('--tool', 'echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm')
}

# Strict mode: expected output must appear AND the driver
# must exit 0. Matches Linux semantics.
$PSNativeCommandUseErrorActionPreference = $false
Expand Down Expand Up @@ -747,7 +774,7 @@ jobs:
$memArgs = @('-m', $memory)
}
$out = & hyperlight-unikraft -q @memArgs `
$kernel --initrd $cpio @mountArgs @argList 2>&1
$kernel --initrd $cpio @mountArgs @toolArgs @argList 2>&1
$rc = $LASTEXITCODE
}
}
Expand Down Expand Up @@ -921,7 +948,14 @@ jobs:

test-examples-passed:
if: always()
needs: [build-example, runtime-test, package-images-for-windows, runtime-test-windows, pyhl-snapshot-test]
needs:
[
build-example,
runtime-test,
package-images-for-windows,
runtime-test-windows,
pyhl-snapshot-test,
]
runs-on: ubuntu-latest
permissions: {}
steps:
Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ This project enables running Linux applications (Python, Node.js, Go, Rust, C/C+

### Key Features

- **Thin, opt-in host surface** — by default, the guest has no access to the host filesystem, network, or any host functions. When you enable features like `--mount`, `--net`, or `--enable-tools`, a single `__dispatch` JSON-RPC bridge is registered as the only guest→host channel. See [HOST_FUNCTIONS.md](HOST_FUNCTIONS.md) for the full list of dispatchable operations
- **Thin, opt-in host surface** — by default, the guest has no access to the host filesystem, network, or custom host tools. When you enable capabilities like `--mount`, `--net`, or `--tool`, a single `__dispatch` JSON-RPC bridge is registered as the only guest→host channel. See [host_functions.md](docs/host_functions.md) for the full list of dispatchable operations
- **Identity-mapped memory** - Simplified memory layout (vaddr == paddr)
- **Generic cmdline mechanism** - Pass arguments to any application via `-- arg1 arg2 ...`
- **Fast cold start** - Hyperlight's lightweight design enables millisecond startup times
Expand Down Expand Up @@ -182,6 +182,7 @@ just run
| `helloworld-c` | Static PIE C binary | Compiled with `musl-gcc` |
| `rust` | Static PIE Rust binary | Compiled with `rustc --target x86_64-unknown-linux-musl` |
| `python` | CPython 3.12 | Rootfs from Docker, script passed via cmdline |
| `python-tools` | CPython 3.12 + host function call | Calls an echo Wasm host function registered with `--tool echo=...` |
| `go` | Static PIE Go binary | Compiled with musl via Docker for CGO support |
| `nodejs` | Node.js 21 | Rootfs from Alpine, script passed via cmdline |
| `hostfs-posix-c` | C + unmodified POSIX | `open`/`read`/`write`/`mkdir` against `/host`, forwarded by `lib/hostfs` |
Expand Down Expand Up @@ -281,7 +282,18 @@ Options:
-m, --memory <MEMORY> Memory allocation (e.g., 256Mi, 512Mi, 1Gi) [default: 512Mi]
--stack <STACK> Stack size (e.g., 8Mi) [default: 8Mi]
-q, --quiet Quiet mode — suppress host-side status messages
--enable-tools Enable tool dispatch via __dispatch host function
--tool <NAME=WASM> Register a WASIp1 module as a host tool (requires wasm-host-fns)
--tool-wasi-dir <HOST[:GUEST]>
Preopen a read-write host directory for Wasm tools
--tool-wasi-dir-ro <HOST[:GUEST]>
Preopen a read-only host directory for Wasm tools
--tool-wasi-env <KEY=VALUE>
Set an environment variable for Wasm tools
--tool-wasi-env-inherit <KEY>
Inherit one host environment variable into Wasm tools
--tool-wasi-fuel <FUEL> Fuel units available to each Wasm tool call [default: 100000000]
--tool-wasi-output-limit <SIZE>
Maximum stdout or stderr captured from one Wasm tool call [default: 1Mi]
--mount <HOST[:GUEST]> Preopen a host directory for the guest's sandboxed filesystem
(repeatable; default guest path: /host)
--net Enable guest networking (off by default)
Expand All @@ -297,6 +309,20 @@ Options:
-V, --version Print version
```

### Wasm host tools

Build the CLI with the optional `wasm-host-fns` feature to register host-side custom tools from WASIp1 modules:

```bash
cargo build --manifest-path host/Cargo.toml --release --features wasm-host-fns --bin hyperlight-unikraft
rustup target add wasm32-wasip1
cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1
hyperlight-unikraft kernel --initrd app.cpio \
--tool echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm
```

The guest still calls the existing `__dispatch` envelope, for example `{"name":"echo","args":{"message":"hello"}}`. The Wasm handler runs on the host in Wasmtime, receives that request JSON on stdin, and writes either a raw JSON result or `{"result": ...}` / `{"error": "..."}` to stdout. WASI filesystem and environment access are off unless granted with `--tool-wasi-dir`, `--tool-wasi-dir-ro`, `--tool-wasi-env`, or `--tool-wasi-env-inherit`.

## Project Structure

```
Expand Down
Loading
Loading