Skip to content

Commit 897275c

Browse files
authored
Merge pull request #91 from cachebag/main
Feature-gated Wasm host tools for the CLI
2 parents 4460ae2 + 027c63d commit 897275c

14 files changed

Lines changed: 3110 additions & 215 deletions

File tree

.github/workflows/host-checks.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ jobs:
6060
- name: Cargo clippy (warnings are errors)
6161
run: cargo clippy --all-targets --locked -- -D warnings
6262

63+
- name: Cargo clippy with Wasm host tools
64+
run: cargo clippy --bin hyperlight-unikraft --tests --locked --features wasm-host-fns -- -D warnings
65+
6366
- name: Enable KVM permissions
6467
working-directory: .
6568
run: |
@@ -75,6 +78,11 @@ jobs:
7578
RUST_BACKTRACE: '1'
7679
run: cargo test --all-targets --locked
7780

81+
- name: Cargo test with Wasm host tools
82+
env:
83+
RUST_BACKTRACE: '1'
84+
run: cargo test --bin hyperlight-unikraft --locked --features wasm-host-fns wasm_host_fns::tests
85+
7886
host-checks-passed:
7987
if: always()
8088
needs: [checks]

.github/workflows/test-examples.yml

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ on:
77
push:
88
branches: [main]
99
paths:
10-
- 'examples/**'
11-
- 'runtimes/**'
12-
- 'host/**'
13-
- '.github/workflows/test-examples.yml'
10+
- "examples/**"
11+
- "runtimes/**"
12+
- "host/**"
13+
- ".github/workflows/test-examples.yml"
1414

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

6969
- uses: actions/setup-go@v5
7070
with:
71-
go-version: '1.25.1'
71+
go-version: "1.25.1"
7272
cache: false
7373

7474
- name: Install just
@@ -92,7 +92,7 @@ jobs:
9292
- name: Build local-python-base images (python-agent-driver only)
9393
if: matrix.example == 'python-agent-driver'
9494
env:
95-
DOCKER_BUILDKIT: '0'
95+
DOCKER_BUILDKIT: "0"
9696
run: |
9797
docker build --target base -t local-python-base-dev:latest \
9898
-f runtimes/python.Dockerfile runtimes/
@@ -102,7 +102,7 @@ jobs:
102102
- name: Build rootfs
103103
working-directory: examples/${{ matrix.example }}
104104
env:
105-
DOCKER_BUILDKIT: '0'
105+
DOCKER_BUILDKIT: "0"
106106
run: |
107107
just rootfs
108108
@@ -185,8 +185,9 @@ jobs:
185185
args: "-- /hello.py"
186186
expect: "Hello from Python on Hyperlight"
187187
- example: python-tools
188-
args: "--enable-tools -- /test_tools.py"
188+
args: "-- /test_tools.py"
189189
expect: "Tool returned"
190+
needs_echo_tool: true
190191
- example: nodejs
191192
args: "-- /app/hello.js"
192193
expect: "Hello from Node.js on Hyperlight"
@@ -280,13 +281,19 @@ jobs:
280281
if: steps.kvm_check.outputs.available == 'true'
281282
run: |
282283
cd host
283-
cargo build --release --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run
284+
cargo build --release --features wasm-host-fns --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run
284285
sudo cp target/release/hyperlight-unikraft /usr/local/bin/
285286
287+
- name: Build echo Wasm host function
288+
if: steps.kvm_check.outputs.available == 'true' && matrix.needs_echo_tool == true
289+
run: |
290+
rustup target add wasm32-wasip1
291+
cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1
292+
286293
- name: Build local-python-base images (python-agent-driver only)
287294
if: steps.kvm_check.outputs.available == 'true' && matrix.example == 'python-agent-driver'
288295
env:
289-
DOCKER_BUILDKIT: '0'
296+
DOCKER_BUILDKIT: "0"
290297
run: |
291298
docker build --target base -t local-python-base-dev:latest \
292299
-f runtimes/python.Dockerfile runtimes/
@@ -297,7 +304,7 @@ jobs:
297304
if: steps.kvm_check.outputs.available == 'true'
298305
working-directory: examples/${{ matrix.example }}
299306
env:
300-
DOCKER_BUILDKIT: '0'
307+
DOCKER_BUILDKIT: "0"
301308
run: |
302309
just rootfs
303310
kraft-hyperlight --no-prompt build --plat hyperlight --arch x86_64 || true
@@ -348,14 +355,20 @@ jobs:
348355
mkdir -p "$mount_dir"
349356
mount_args="--mount $mount_dir:/host"
350357
fi
358+
359+
tool_args=""
360+
if [ "${{ matrix.needs_echo_tool }}" = "true" ]; then
361+
tool_args="--tool echo=../echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm"
362+
fi
363+
351364
# HTTP server examples: start in background, poll, curl, kill.
352365
http_port="${{ matrix.http_port }}"
353366
if [ -n "$http_port" ]; then
354367
mem_args=""
355368
if [ -n "$memory" ]; then
356369
mem_args="-m $memory"
357370
fi
358-
hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" ${{ matrix.args }} &
371+
hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args $tool_args ${{ matrix.args }} &
359372
server_pid=$!
360373
sleep 3
361374
ready=0
@@ -396,7 +409,7 @@ jobs:
396409
if [ -n "$memory" ]; then
397410
mem_args="-m $memory"
398411
fi
399-
cmd=(timeout 120 hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args ${{ matrix.args }})
412+
cmd=(timeout 120 hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" $mount_args $tool_args ${{ matrix.args }})
400413
;;
401414
esac
402415
set +e
@@ -456,7 +469,7 @@ jobs:
456469

457470
- uses: actions/setup-go@v5
458471
with:
459-
go-version: '1.25.1'
472+
go-version: "1.25.1"
460473
cache: false
461474

462475
- name: Install just
@@ -480,7 +493,7 @@ jobs:
480493
- name: Build local-python-base images (python-agent-driver only)
481494
if: matrix.example == 'python-agent-driver'
482495
env:
483-
DOCKER_BUILDKIT: '0'
496+
DOCKER_BUILDKIT: "0"
484497
run: |
485498
docker build --target base -t local-python-base-dev:latest \
486499
-f runtimes/python.Dockerfile runtimes/
@@ -490,7 +503,7 @@ jobs:
490503
- name: Build rootfs + kernel
491504
working-directory: examples/${{ matrix.example }}
492505
env:
493-
DOCKER_BUILDKIT: '0'
506+
DOCKER_BUILDKIT: "0"
494507
run: |
495508
just rootfs
496509
kraft-hyperlight --no-prompt build --plat hyperlight --arch x86_64 || true
@@ -562,8 +575,9 @@ jobs:
562575
args: "-- /hello.py"
563576
expect: "Hello from Python on Hyperlight"
564577
- example: python-tools
565-
args: "--enable-tools -- /test_tools.py"
578+
args: "-- /test_tools.py"
566579
expect: "Tool returned"
580+
needs_echo_tool: true
567581
- example: nodejs
568582
args: "-- /app/hello.js"
569583
expect: "Hello from Node.js on Hyperlight"
@@ -631,12 +645,19 @@ jobs:
631645
shell: pwsh
632646
run: |
633647
cd host
634-
cargo build --release --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run --bin pyhl
648+
cargo build --release --features wasm-host-fns --bin hyperlight-unikraft --bin multifn-test --bin pydriver-run --bin pyhl
635649
Copy-Item target\release\hyperlight-unikraft.exe $env:USERPROFILE\.cargo\bin\ -Force
636650
Copy-Item target\release\multifn-test.exe $env:USERPROFILE\.cargo\bin\ -Force
637651
Copy-Item target\release\pydriver-run.exe $env:USERPROFILE\.cargo\bin\ -Force
638652
Copy-Item target\release\pyhl.exe $env:USERPROFILE\.cargo\bin\ -Force
639653
654+
- name: Build echo Wasm host function
655+
if: matrix.needs_echo_tool == true
656+
shell: pwsh
657+
run: |
658+
rustup target add wasm32-wasip1
659+
cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1
660+
640661
- name: Download prebuilt image
641662
uses: actions/download-artifact@v4
642663
with:
@@ -652,6 +673,7 @@ jobs:
652673
$driver = '${{ matrix.driver }}'
653674
$runArgs = '${{ matrix.args }}'
654675
$needsMount = '${{ matrix.needs_mount }}'
676+
$needsEchoTool = '${{ matrix.needs_echo_tool }}'
655677
656678
# Read memory from the example's Justfile (single source of truth).
657679
$memory = ''
@@ -673,6 +695,11 @@ jobs:
673695
$mountArgs = @('--mount', ($mountDir + ':/host'))
674696
}
675697
698+
$toolArgs = @()
699+
if ($needsEchoTool -eq 'true') {
700+
$toolArgs = @('--tool', 'echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm')
701+
}
702+
676703
# Strict mode: expected output must appear AND the driver
677704
# must exit 0. Matches Linux semantics.
678705
$PSNativeCommandUseErrorActionPreference = $false
@@ -747,7 +774,7 @@ jobs:
747774
$memArgs = @('-m', $memory)
748775
}
749776
$out = & hyperlight-unikraft -q @memArgs `
750-
$kernel --initrd $cpio @mountArgs @argList 2>&1
777+
$kernel --initrd $cpio @mountArgs @toolArgs @argList 2>&1
751778
$rc = $LASTEXITCODE
752779
}
753780
}
@@ -921,7 +948,14 @@ jobs:
921948
922949
test-examples-passed:
923950
if: always()
924-
needs: [build-example, runtime-test, package-images-for-windows, runtime-test-windows, pyhl-snapshot-test]
951+
needs:
952+
[
953+
build-example,
954+
runtime-test,
955+
package-images-for-windows,
956+
runtime-test-windows,
957+
pyhl-snapshot-test,
958+
]
925959
runs-on: ubuntu-latest
926960
permissions: {}
927961
steps:

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ This project enables running Linux applications (Python, Node.js, Go, Rust, C/C+
4949

5050
### Key Features
5151

52-
- **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
52+
- **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
5353
- **Identity-mapped memory** - Simplified memory layout (vaddr == paddr)
5454
- **Generic cmdline mechanism** - Pass arguments to any application via `-- arg1 arg2 ...`
5555
- **Fast cold start** - Hyperlight's lightweight design enables millisecond startup times
@@ -182,6 +182,7 @@ just run
182182
| `helloworld-c` | Static PIE C binary | Compiled with `musl-gcc` |
183183
| `rust` | Static PIE Rust binary | Compiled with `rustc --target x86_64-unknown-linux-musl` |
184184
| `python` | CPython 3.12 | Rootfs from Docker, script passed via cmdline |
185+
| `python-tools` | CPython 3.12 + host function call | Calls an echo Wasm host function registered with `--tool echo=...` |
185186
| `go` | Static PIE Go binary | Compiled with musl via Docker for CGO support |
186187
| `nodejs` | Node.js 21 | Rootfs from Alpine, script passed via cmdline |
187188
| `hostfs-posix-c` | C + unmodified POSIX | `open`/`read`/`write`/`mkdir` against `/host`, forwarded by `lib/hostfs` |
@@ -281,7 +282,18 @@ Options:
281282
-m, --memory <MEMORY> Memory allocation (e.g., 256Mi, 512Mi, 1Gi) [default: 512Mi]
282283
--stack <STACK> Stack size (e.g., 8Mi) [default: 8Mi]
283284
-q, --quiet Quiet mode — suppress host-side status messages
284-
--enable-tools Enable tool dispatch via __dispatch host function
285+
--tool <NAME=WASM> Register a WASIp1 module as a host tool (requires wasm-host-fns)
286+
--tool-wasi-dir <HOST[:GUEST]>
287+
Preopen a read-write host directory for Wasm tools
288+
--tool-wasi-dir-ro <HOST[:GUEST]>
289+
Preopen a read-only host directory for Wasm tools
290+
--tool-wasi-env <KEY=VALUE>
291+
Set an environment variable for Wasm tools
292+
--tool-wasi-env-inherit <KEY>
293+
Inherit one host environment variable into Wasm tools
294+
--tool-wasi-fuel <FUEL> Fuel units available to each Wasm tool call [default: 100000000]
295+
--tool-wasi-output-limit <SIZE>
296+
Maximum stdout or stderr captured from one Wasm tool call [default: 1Mi]
285297
--mount <HOST[:GUEST]> Preopen a host directory for the guest's sandboxed filesystem
286298
(repeatable; default guest path: /host)
287299
--net Enable guest networking (off by default)
@@ -297,6 +309,20 @@ Options:
297309
-V, --version Print version
298310
```
299311

312+
### Wasm host tools
313+
314+
Build the CLI with the optional `wasm-host-fns` feature to register host-side custom tools from WASIp1 modules:
315+
316+
```bash
317+
cargo build --manifest-path host/Cargo.toml --release --features wasm-host-fns --bin hyperlight-unikraft
318+
rustup target add wasm32-wasip1
319+
cargo build --manifest-path examples/echo-wasm-host-fxn/Cargo.toml --release --target wasm32-wasip1
320+
hyperlight-unikraft kernel --initrd app.cpio \
321+
--tool echo=examples/echo-wasm-host-fxn/target/wasm32-wasip1/release/echo-wasm-host-fxn.wasm
322+
```
323+
324+
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`.
325+
300326
## Project Structure
301327

302328
```

0 commit comments

Comments
 (0)