Skip to content
Open
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
16 changes: 16 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# WIT_WORLD is consumed at COMPILE time by:
# - hyperlight_wasm_macro (proc-macro) -> std::env::var_os("WIT_WORLD").unwrap()
# - hyperlight_wasm/build.rs -> rerun-if-env-changed=WIT_WORLD
# - hyperlight_wasm_runtime/build.rs -> emits cfg(component) IFF WIT_WORLD is set
#
# Without it, hyperlight_wasm_runtime falls back to the legacy flatbuffer-based
# host-function ABI, while hyperlight-wasm-sandbox (built with WIT_WORLD via the
# proc-macro's host_bindgen!) uses the component-model ABI. Mixing the two yields
# "GuestError: Host function vector parameter missing length" at sandbox start.
#
# `just wasm test` exports WIT_WORLD via its recipe, but bare `cargo` invocations
# (e.g. from an IDE or a developer running `cargo test --manifest-path ...`)
# would otherwise miss it. Cargo looks for `.cargo/config.toml` by walking up the
# CWD tree, so we put it at the repo root to cover every workspace cargo call.
[env]
WIT_WORLD = { value = "src/wasm_sandbox/wit/sandbox-world.wasm", relative = true }
1 change: 0 additions & 1 deletion .copilot/skills/justfile-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ This repo uses a **hierarchical Justfile** structure with a matching **GitHub Ac
Justfile (root) ← orchestrates everything
├── mod wasm 'src/wasm_sandbox/Justfile'
├── mod js 'src/javascript_sandbox/Justfile'
├── mod nanvix 'src/nanvix_sandbox/Justfile'
├── mod python 'src/sdk/python/Justfile'
└── mod examples_mod 'examples/Justfile'
```
Expand Down
9 changes: 2 additions & 7 deletions .copilot/skills/justfile-ci/references/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Recipe | Delegates to | Purpose |
|--------|-------------|---------|
| `build` | `wasm::build`, `jss::build`, `nanvix::build`, `python::build` | Build everything |
| `build` | `wasm::build`, `jss::build`, `python::build` | Build everything |
| `test` | `test-rust`, `wasm::test`, `python::python-test` | All tests |
| `test-rust` | (direct cargo) | Core crate unit + integration tests |
| `lint` | `lint-rust`, `wasm::lint`, `js::lint`, `python::lint` | All linters |
Expand All @@ -21,8 +21,7 @@
ci.yml
├── rust — fmt-check-rust, lint-rust, test-rust
├── wasm-sandbox — wasm build, lint, test, examples + python fmt-check/lint/build/examples/python-test/fuzz/benchmark/integration-examples
├── javascript-sandbox — js build, lint, test, examples
└── nanvix-sandbox — nanvix build (examples skipped pending upstream)
└── javascript-sandbox — js build, lint, test, examples
```

## Subproject Justfile Responsibilities
Expand All @@ -39,10 +38,6 @@ ci.yml
- Recipes: `build`, `test`, `examples`, `lint`
- `examples` is a flat recipe listing all `cargo run` commands

### nanvix (`src/nanvix_sandbox/Justfile`)
- Standalone build, excluded from workspace
- Recipes: `build`, `examples` (currently skipped)

### python (`src/sdk/python/Justfile`)
- Manages `uv`, `maturin`, `ruff` tooling
- Recipes: `build`, `fmt`, `fmt-check`, `lint`, `examples`, `python-test`, `python-fuzz`, `python-sandbox-benchmark`, `python-publish`
Expand Down
32 changes: 9 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ jobs:
sudo udevadm trigger --name-match=kvm
sudo chmod 666 /dev/kvm

- name: Check WIT artifact is in sync with .wit source
# sandbox-world.wasm is the compiled WIT used by host_bindgen!. It
# is consumed by transitive deps (hyperlight-wasm-runtime) before
# our crate's build scripts run, so it must live in git. Detect any
# drift between the committed artifact and the .wit source.
run: |
just wasm guest-compile-wit
git diff --exit-code -- src/wasm_sandbox/wit/sandbox-world.wasm

- name: Build
run: just wasm build

Expand Down Expand Up @@ -411,26 +420,3 @@ jobs:

- name: Run examples
run: just js examples

nanvix-sandbox:
name: Nanvix Sandbox · build
runs-on: ubuntu-latest #current linux only
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
with:
toolchain: nightly
rustflags: ""

- name: Install just
run: cargo install --locked just

- name: Build
run: just nanvix build

# NOTE: nanvix examples are skipped until hyperlight-nanvix updates to
# the new nanvix registry API (qjs/python3 are now separate packages).
# See: https://github.com/hyperlight-dev/hyperlight-nanvix
# - name: Run examples
# run: just nanvix examples
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ target/
.venv/
*.aot
*.wasm
# Exception: the compiled WIT artifact is checked in as a build input.
# host_bindgen! reads this file *before* our build scripts can run (it is
# consumed by transitive deps like hyperlight-wasm-runtime), so the file
# must exist from `git clone` time. CI regenerates and diffs to catch drift.
!src/wasm_sandbox/wit/sandbox-world.wasm
__pycache__/
*.so
*.pyd
Expand Down
4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ members = [
"src/sdk/python/hyperlight_js_backend",
"src/sdk/dotnet/ffi",
]
# nanvix_sandbox requires nightly Rust (nanvix uses #![feature(never_type)])
exclude = [
"src/nanvix_sandbox",
]
resolver = "2"

[workspace.package]
Expand Down
3 changes: 1 addition & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ set unstable := true

mod wasm 'src/wasm_sandbox/Justfile'
mod js 'src/javascript_sandbox/Justfile'
mod nanvix 'src/nanvix_sandbox/Justfile'
mod python 'src/sdk/python/Justfile'
mod dotnet 'src/sdk/dotnet/Justfile'
mod examples_mod 'examples/Justfile'
Expand All @@ -14,7 +13,7 @@ clean: wasm::clean python::clean dotnet::clean

#### BUILD TARGETS ####

build target=default-target: (wasm::build target) (js::build target) nanvix::build python::build (dotnet::build target)
build target=default-target: (wasm::build target) (js::build target) python::build (dotnet::build target)

lint: lint-rust wasm::lint js::lint python::lint

Expand Down
30 changes: 0 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Supported backends:

- [Wasm Component Sandbox](#wasm-component-sandbox) (Python/Javascript or provide your own)
- [HyperlightJS Sandbox](#hyperlightjs-sandbox)
- [Nanvix Sandbox](#nanvix-sandbox)

## Overview

Expand Down Expand Up @@ -195,35 +194,6 @@ console.log('10 + 20 = ' + sum);

See [examples](./src/javascript_sandbox/examples/) for file I/O and network demos.

### Nanvix Sandbox

A microkernel-based backend built on [hyperlight-nanvix](https://github.com/hyperlight-dev/hyperlight-nanvix) that runs JavaScript or Python inside a Nanvix VM. Currently limited to basic code execution with stdout capture -- no host tools, file I/O, networking, or snapshot support yet.

```rust
use hyperlight_nanvix_sandbox::{NanvixJavaScript, NanvixPython};
use hyperlight_sandbox::Sandbox;

fn main() {
// JavaScript
let mut js = Sandbox::builder()
.guest(NanvixJavaScript)
.build()
.expect("failed to create JS sandbox");

let result = js.run(r#"console.log("Hello from Nanvix JS!");"#).unwrap();
print!("{}", result.stdout);

// Python
let mut py = Sandbox::builder()
.guest(NanvixPython)
.build()
.expect("failed to create Python sandbox");

let result = py.run(r#"print("Hello from Nanvix Python!")"#).unwrap();
print!("{}", result.stdout);
}
```

## Building

Tool requirements:
Expand Down
1 change: 0 additions & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Bump the version in **all** manifest files. For example, to go from `0.1.0` →
### Rust (Cargo)

- `Cargo.toml` — `[workspace.package] version`
- `src/nanvix_sandbox/Cargo.toml` — `[package] version` (excluded from workspace, must be updated manually)

All other workspace member crates inherit the version automatically.

Expand Down
14 changes: 1 addition & 13 deletions docs/end-user-overview-slides.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,7 @@ result = sandbox.run('console.log(2 + 3)')

---

# Guest Support: Nanvix & Extensibility

## Nanvix guests (Rust API)

```rust
// JavaScript via QuickJS in a Nanvix microkernel
let mut sandbox = SandboxBuilder::new().guest(NanvixJavaScript).build()?;
let result = sandbox.run(r#"console.log("Hello from Nanvix!")"#)?;

// Python via Python in a Nanvix microkernel
let mut sandbox = SandboxBuilder::new().guest(NanvixPython).build()?;
let result = sandbox.run(r#"print("Hello from Nanvix!")"#)?;
```
# Guest Support & Extensibility

## Broader Wasm/WASI guest model

Expand Down
105 changes: 105 additions & 0 deletions src/hyperlight_sandbox/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! Scoped-credential registry for outgoing HTTP requests.
//!
//! A [`CredentialEntry`] binds a logical credential identifier to the
//! metadata required to inject a token header at request time:
//!
//! * `target` — URL-prefix scope. The outgoing-handler only injects
//! the credential when the request URL starts with this prefix.
//! * `header` — HTTP header name (e.g. `"Authorization"`).
//! * `prefix` — Value prefix prepended to the resolved token
//! (e.g. `"Bearer "`).
//! * `resolver` — A host-side callback invoked on every credentialed
//! outgoing request to produce a fresh secret value. The host calls
//! the resolver synchronously from the WASI HTTP dispatch path, so
//! implementations should be fast and (where appropriate) memoise
//! internally. Errors returned by the resolver surface to the guest
//! as a request-level dispatch failure with a host-redacted message.
//!
//! The registry is populated by the host before the guest runs.
//! Guests bind a credential to a specific outgoing request via WIT
//! `attach`.

use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};

/// Host-side callback that produces the secret token value for a
/// credential at request-dispatch time.
///
/// The returned `String` is treated as the literal token; the host
/// prepends [`CredentialEntry::prefix`] to it to form the outgoing
/// header value.
///
/// On error, the returned diagnostic string is **dropped** by the
/// outgoing-handler before any guest-visible error is produced — it
/// is neither sent to the guest nor logged by this crate. The wire
/// path surfaces only a fixed `"credential resolver failed"`
/// indication. Resolver authors who need diagnostics should record
/// them inside the resolver itself (e.g. via the host's own logger)
/// before returning the `Err`.
pub type ResolverFn = Arc<dyn Fn() -> Result<String, String> + Send + Sync>;

/// Metadata for a single scoped credential.
#[derive(Clone)]
pub struct CredentialEntry {
/// URL-prefix scope. Only requests whose URL starts with this
/// value are eligible for credential injection.
pub target: String,

/// HTTP header name to set (e.g. `"Authorization"`).
pub header: String,

/// Value prefix prepended to the resolved token
/// (e.g. `"Bearer "`). May be empty.
pub prefix: String,

/// Resolver callback. Invoked on every credentialed outgoing
/// request; see [`ResolverFn`] for the contract.
pub resolver: ResolverFn,
}

impl fmt::Debug for CredentialEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// The resolver is a function pointer that may close over secret
// material; we never want it (or its captures) to appear in a
// log line, panic message, or `dbg!` output.
f.debug_struct("CredentialEntry")
.field("target", &self.target)
.field("header", &self.header)
.field("prefix", &self.prefix)
.field("resolver", &"<callback>")
.finish()
}
}

impl CredentialEntry {
/// Build a [`CredentialEntry`] whose resolver returns a fixed
/// token string on every invocation.
///
/// Convenience constructor for tests, examples, and trivially
/// short-lived secrets. Production callers that need refresh
/// behaviour (managed identities, OAuth, …) should construct
/// the entry directly with a custom [`ResolverFn`].
pub fn with_static_resolver(
target: impl Into<String>,
header: impl Into<String>,
prefix: impl Into<String>,
token: impl Into<String>,
) -> Self {
let token = token.into();
Self {
target: target.into(),
header: header.into(),
prefix: prefix.into(),
resolver: Arc::new(move || Ok(token.clone())),
}
}
}

/// Shared, thread-safe credential registry keyed by credential id.
pub type CredentialRegistry = Arc<Mutex<HashMap<String, CredentialEntry>>>;

/// Creates an empty credential registry.
pub fn empty_registry() -> CredentialRegistry {
Arc::new(Mutex::new(HashMap::new()))
}
Loading
Loading