Repository navigation
Conversation
The lock had fallen behind pyproject.toml: twine is no longer a dev dependency, and the ruff and agent-framework-devui floors moved. uv sync --frozen does not notice; uv lock --check does. Signed-off-by: danbugs <danilochiarlone@gmail.com>
There was a problem hiding this comment.
🟡 Changes recommended
Runtime correctness, Windows image discovery, and release/test integration issues remain unresolved.
9 open issues
Handle unavailable hypervisors in the .NET execution test · New Match the CLI's Windows cache path in the test helper · New Publish the Unikraft sandbox crate in the release workflow · New Use the CLI's Windows cache directory · New Preserve ordering between Python and native stdout writes · New Preserve stderr when stdout and stderr share a partial line · New Pass unbracketed IPv6 hosts to the native allowlist · New Register the persistent guest namespace for pickling · New Skip integration tests when no usable hypervisor is available · New
What changed in this PR
Adds a full-CPython Unikraft backend alongside Wasm and HyperlightJS, using the existing sandbox contract.
Changes:
- Implements VM execution, tool and HTTP bridges, filesystem checks, and snapshots.
- Adds Python and .NET backend selection and configuration.
- Adds tests, examples, documentation, and build/package wiring.
| File | Description |
|---|---|
| src/unikraft_sandbox/tests/relative_output.rs | Tests mount stability across directory changes. |
| src/unikraft_sandbox/tests/python_image.rs | Adds image-backed integration tests. |
| src/unikraft_sandbox/src/prelude.py | Implements the guest Python bridge. |
| src/unikraft_sandbox/src/lib.rs | Implements the Unikraft backend. |
| src/unikraft_sandbox/Justfile | Adds backend development recipes. |
| src/unikraft_sandbox/examples/basics.rs | Demonstrates Rust backend usage. |
| src/unikraft_sandbox/Cargo.toml | Defines the backend crate. |
| src/sdk/python/unikraft_backend/src/lib.rs | Adds native Python bindings. |
| src/sdk/python/unikraft_backend/README.md | Describes the binding package. |
| src/sdk/python/unikraft_backend/pyproject.toml | Configures Python packaging. |
| src/sdk/python/unikraft_backend/hyperlight_sandbox_backend_unikraft/__init__.py | Exports native binding types. |
| src/sdk/python/unikraft_backend/Cargo.toml | Defines the Python extension crate. |
| src/sdk/python/Justfile | Wires backend builds and publishing. |
| src/sdk/python/core/tests/test_unikraft_options.py | Tests Python option forwarding. |
| src/sdk/python/core/tests/test_network_permissions.py | Extends network validation coverage. |
| src/sdk/python/core/README.md | Documents the installation extra. |
| src/sdk/python/core/pyproject.toml | Adds the Unikraft dependency extra. |
| src/sdk/python/core/hyperlight_sandbox/__init__.py | Adds backend selection and options. |
| src/sdk/python/core/examples/unikraft_basics.py | Demonstrates Python backend usage. |
| src/sdk/dotnet/README.md | Documents .NET backend usage. |
| src/sdk/dotnet/ffi/src/lib.rs | Adds native dispatch and configuration. |
| src/sdk/dotnet/ffi/Cargo.toml | Adds the backend dependency. |
| src/sdk/dotnet/core/Tests/HyperlightSandbox.Tests/UnikraftBackendTests.cs | Tests builder validation. |
| src/sdk/dotnet/core/Tests/HyperlightSandbox.Tests/CodeExecutionToolTests.cs | Tests tool integration. |
| src/sdk/dotnet/core/PInvoke/SafeNativeMethods.cs | Declares new native configuration calls. |
| src/sdk/dotnet/core/Extensions.AI/CodeExecutionTool.cs | Adds Unikraft initialization code. |
| src/sdk/dotnet/core/Api/SandboxBuilder.cs | Adds backend-specific builder options. |
| src/sdk/dotnet/core/Api/SandboxBackend.cs | Adds the Unikraft enum value. |
| src/sdk/dotnet/core/Api/Sandbox.cs | Applies native backend settings. |
| src/hyperlight_sandbox/src/network.rs | Exposes granted network permissions. |
| src/hyperlight_sandbox/src/cap_fs.rs | Adds directory identity and output validation. |
| README.md | Documents backend capabilities and differences. |
| pyproject.toml | Registers the Python workspace package. |
| Justfile | Integrates backend build and test recipes. |
| Cargo.toml | Registers crates and dependencies. |
| Cargo.lock | Locks the expanded dependency graph. |
Review effort: Balanced
A backend that mounts host directories into its guest needs the input directory's host path, as it already gets the output one; one that enforces the allowlist on raw sockets needs the permissions granted. CapFs::validate_output rescans the output directory after such a guest wrote it directly and applies the rules CapFs applies to writes made through it (limits, no symbolic or hard links, traversal bounds), plus no special files, which only a direct writer can make. CapFs::is_same_dir tells whether a path still names the directory CapFs opened, for a backend that hands the guest that path. Signed-off-by: danbugs <danilochiarlone@gmail.com>
hyperlight-unikraft-sandbox runs full CPython in a Unikraft micro-VM, from the published python, python-shell and agent rootfs images (the agent image adds numpy, pandas, scipy and scikit-learn), behind the same Guest / GuestSandbox contract as the Wasm and HyperlightJS backends. A prelude loaded at boot gives guest code call_tool, http_get and http_post as the Wasm Python guest has them, and the guest function every run calls. Requests and replies over the 64 KiB host call limit cross it in pieces, output in chunks bounded by their encoded size; the host holds at most 64 MiB of unread pieces per run. Exit statuses are a CPython process's, 0 to 255, leaving -1 to a failed guest. HTTP goes through the host allowlist; guest sockets are opt-in (native_network) and allowed by host name only, an unresolvable name being left out rather than failing every run. Snapshot and restore cover the whole micro-VM. /input and /output are hostfs mounts of paths resolved at build, and CapFs checks /output after each run (CapFs::validate_output): a run that breaks its rules fails. /output is cleared before each run and after a failed check, through the handle CapFs opened so that the working directory cannot redirect it, of every file and link and of the directories the guest made, however deep; the host's directories are kept. hostfs opens a mount when the guest boots, so before each boot the paths are checked to still be the directories CapFs opened: a replaced one fails the boot until it is put back. It builds against hyperlight-unikraft 0.18.0 and its images. That release's hyperlight-host 0.18 links beside the 0.17 hyperlight-wasm 0.15 and hyperlight-js 0.4 still need: hyperlight-sandbox shares no hyperlight type between backends. The guest's own output stays out of the host process (echo_output(false)), is capped like the rest, and its standard error, the kernel's messages included, goes to the run's stderr when written in whole lines. Child processes, threads and kills work in the guest as of 0.18, and the agent image carries the runtime marker hluk sizes it by. The end-to-end tests skip themselves when the images are not cached (just unikraft pull-images). Signed-off-by: danbugs <danilochiarlone@gmail.com>
Sandbox(backend="unikraft") (also "hyperlight-unikraft" or "hluk") runs
code in a hyperlight-unikraft micro-VM through the new
hyperlight-sandbox-backend-unikraft package, installed with
hyperlight-sandbox[unikraft]. module names a published image the hluk
CLI has cached ("agent" by default, or "python"), module_path any rootfs
image; not both. scratch_size sets the guest memory, and native_network
lets guest sockets reach the allow_domain hosts. These are refused for
the other backends, and heap_size and stack_size for this one, before
any backend is imported; SandboxEnvironment leaves heap_size and
stack_size unset by default to allow it. CodeExecutionTool takes the
backend like the others.
allow_domain validates its methods when called, before or after the
first run, as the other backends do since #249; the network permission
tests cover the backend where it is installed, its python image cached
and a hypervisor present.
The Justfile builds, packages and publishes the backend with the other
two, and unikraft-examples runs unikraft_basics.py.
Signed-off-by: danbugs <danilochiarlone@gmail.com>
SandboxBackend.Unikraft (FFI backend 2) builds a hyperlight-unikraft sandbox from the rootfs image given as the module path. WithScratchSize (hyperlight_sandbox_set_scratch_size) sets its guest memory, at least 1 MiB, in place of the heap and stack sizes, which are refused for it as in the Python SDK; unset, the image's tested size applies. WithNativeNetwork (hyperlight_sandbox_set_native_network) turns on guest sockets, as native_network does in Python. CodeExecutionTool initialises it as it does the Wasm Python guest. Its sandbox is boxed in the FFI's backend enum, being several times the size of the others. Signed-off-by: danbugs <danilochiarlone@gmail.com>
How to run it from Rust and Python, the images it takes, and how it differs from the other backends: one guest memory size, host-mounted /input and /output checked after each run and cleared before it, socket-level networking by host name only, data exchanged in pieces past the 64 KiB host call limit, CPython exit statuses, and hyperlight-unikraft's guest limits. Signed-off-by: danbugs <danilochiarlone@gmail.com>
b2184b7 to
369e70c
Compare
| FilesystemLimitsConfiguration? filesystemLimits, | ||
| SandboxBackend backend = SandboxBackend.Wasm) | ||
| SandboxBackend backend = SandboxBackend.Wasm, | ||
| bool nativeNetwork = false, |
There was a problem hiding this comment.
I am not sure how this would work but I would prefer that the hl-unikraft network goes through the wasi interface (same for the filesystem going through wasi-file).
In hl-js I create a custom guest handler that did this work
. I would imagine we would do something similar in unikraft backend.There was a problem hiding this comment.
The Unikraft guest's network and filesystem now go through the sandbox's own policy, as in the hl-js handler (e942b66, a7609db; hluk side in hyperlight-dev/hyperlight-unikraft#179 and #181). For now this builds against a pinned hluk rev; hluk v0.19.0 will include these changes and the PR will move to it.
Pinned to 47bc9e2, which has NetworkPolicy::Filter (hyperlight-dev/hyperlight-unikraft#179) and FsBackend (#181), until they are released; then this becomes a version bump. The v0.18.0 images run on it unchanged. Signed-off-by: danbugs <danilochiarlone@gmail.com>
With native_network, guest sockets were allowed to the allow_domain hosts by name, resolved into an AllowList at boot, and an allowlist change re-booted the guest from a snapshot to take effect. A name that did not resolve then was left out until the next change. hyperlight-unikraft's NetworkPolicy::Filter asks the embedder about each DNS question and connect instead, so the guest's sockets are checked against the NetworkPermissions http_get uses, as they are at that moment: a TCP connection to an entry's host on its port (the one the URL names, else its scheme's). An allow_domain applies from the next connect, nothing is resolved up front, and the re-boot and its directory checks go. Signed-off-by: danbugs <danilochiarlone@gmail.com>
A hyperlight-unikraft guest asks the host for each file operation by path, not through WASI descriptors. stat_path, read_path, write_path, create_dir_path, remove_path, truncate_path, list_path and rename_path serve it by the rules the descriptor operations apply: the directory permissions, and for the output the filesystem limits (checked before the host directory changes, and kept as files are removed, truncated or renamed over), no hard-linked files, and the bounds the output scan walks (64 levels, a directory moved included, and 100,000 entries). Only regular files are read, so a FIFO cannot block the call. Errors are io::Errors, as the guest's file calls should see them. Signed-off-by: danbugs <danilochiarlone@gmail.com>
The guest's /input and /output are now hostfs mounts whose operations CapFs serves, through hyperlight-unikraft's FsBackend, instead of host directories the guest writes directly and CapFs checks after the run. A write past the filesystem limits, a link, or a path deeper than the output scan walks now fails in the guest (EFBIG, EDQUOT, EACCES, EINVAL) before the host directory changes, as in the other backends. The check before and after each run stays, for what no single operation shows: what the host changes in /output during a run, and more than 100,000 entries. The guest no longer holds the directories by path, so the check that they were not replaced before boot goes, and with it CapFs::is_same_dir and CapFs::input_path, which only it used. Signed-off-by: danbugs <danilochiarlone@gmail.com>

Adds a third backend that runs full CPython in a Unikraft micro-VM via hyperlight-unikraft 0.18.0. The
agentimage includes numpy, pandas, scipy and scikit-learn. The backend follows the same Guest/GuestSandbox contract as the Wasm and HyperlightJS backends, soSandbox(backend="unikraft")gives the samerun, tools, network allowlist,/inputand/output, and snapshot/restore.hyperlight-unikraft-sandboxcrate:call_tool,http_getandhttp_post, with large payloads chunked both ways and per-run caps on what the host holds;/inputand/outputare host mounts./outputis checked with CapFs's own rules (sizes, links, depth, entries, special files) after every run, and before every run for what the host put there;/outputpersist across runs and are cleared on snapshot restore, as in the other backends (Preserve output files across sandbox runs #250);native_network);CapFs::validate_outputandCapFs::is_same_dir, plusinput_pathandpermissions()accessors, for backends whose guests write host directories.Sandbox(backend="unikraft")withscratch_sizeandnative_network, thehyperlight-sandbox[unikraft]extra, an example, andCodeExecutionToolsupport.allow_domainvalidates methods up front, as Validate queued Python network methods immediately #249 does for the other backends.SandboxBackend.Unikraft,WithScratchSize,WithNativeNetworkandCodeExecutionToolsupport.uv.lock.SandboxEnvironment.heap_sizeandstack_sizenow default toNone. The Wasm and JS backends still get the same values as before.