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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked.

## [Unreleased]

### Added

- **Registered and implemented `astrid:http@1.1.0` host-side — the per-request control surface for the HTTP client — alongside the frozen `@1.0.0`.** Both versions are now registered in the wasmtime linker and backed by ONE shared implementation, so existing capsules are untouched and new ones opt in package-by-package. A capsule importing `@1.1.0` gets caller-set controls via an all-optional `request-options` record (an empty value reproduces `@1.0.0` exactly): four-phase timeouts (`connect` / `first-byte` / `between-bytes` / `total`), redirect policy (`follow` / `error` / `manual`) with mandatory per-hop SSRF re-validation, response and decompressed-body size caps, an `auto-decompress` toggle, `https-only` scheme enforcement, and subresource integrity (`sha256` / `sha384` / `sha512`); plus `response-meta` (final URL after redirects, redirect count, elapsed ms, wire bytes) on every buffered response. The `@1.0.0` `http-request` / `http-stream-start` are thin shims that reproduce prior behaviour exactly (follow ≤10 redirects, 30 s timeout, 10 MB cap) and delegate to the same backend. **Security:** redirects on BOTH the buffered and streaming paths are now followed MANUALLY by the host, so every hop re-runs the full airlock — scheme check, the async per-capsule allow-list gate, and the egress airlock — with `Authorization` / `Cookie` stripped on a cross-origin hop and IP-literal targets blocked. This closes a redirect-SSRF gap on the streaming path where the per-capsule gate previously ran only on the initial URL (the IP airlock already ran per hop). `astrid:http` is the first host package to ship two versions, so this also threads the multi-version WIT staging (`build.rs` keys each staged `deps/` dir by `name@version`) and the dual-trait registration. Deferred to follow-ups (honest stubs, not silent gaps): the `http-upload` streaming request body, response `trailers` extraction, and un-stubbing the stream pollable / `body-stream`. No `@1.0.0` shape change — it is frozen and untouched. Closes #1049. Part of #1012.

### Changed

- **CI clippy checks updated to run with `--all-targets` and all pre-existing clippy lints resolved across test, example, and benchmark targets.** Refactored test-only code to address dead code, field reassignments with default, misplaced imports, and excessively long functions workspace-wide. Closes #1004.
Expand Down
8 changes: 7 additions & 1 deletion crates/astrid-capsule/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ astrid-workspace = { workspace = true }
anyhow = { workspace = true }
arc-swap = { workspace = true }
async-trait = { workspace = true }
base64 = { workspace = true }
blake3 = { workspace = true }
bytes = "1"
dashmap = { workspace = true }
Expand All @@ -30,10 +31,15 @@ metrics = { workspace = true }
notify = "7"
parking_lot = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true }
# `astrid:http@1.1.0` `auto-decompress` toggles gzip/brotli/deflate/zstd per
# request; those builder methods are feature-gated in reqwest. Enabled here so
# the host can honour (and disable) decompression. The workspace base dep stays
# minimal; Cargo unions these features in for the capsule crate's build.
reqwest = { workspace = true, features = ["gzip", "brotli", "deflate", "zstd"] }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
socket2 = "0.6"
tempfile = { workspace = true }
thiserror = { workspace = true }
Expand Down
17 changes: 12 additions & 5 deletions crates/astrid-capsule/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
//! `unicity-astrid/wit`) with per-domain packages under `host/`. wasmtime's
//! bindgen expects a single root WIT directory with one package per
//! `deps/<name>/` subdir, so we copy each `host/<pkg>@<ver>.wit` into
//! `wit-staging/deps/astrid-<pkg>/<pkg>@<ver>.wit`. The synthetic kernel
//! world is supplied via the `inline:` option in `bindings.rs`.
//! `wit-staging/deps/astrid-<pkg>@<ver>/<pkg>@<ver>.wit`. Keying the dest
//! dir by the full `<pkg>@<ver>` stem (not just the package name) keeps two
//! versions of the same package (e.g. `http@1.0.0` and `http@1.1.0`) in
//! separate `deps/` dirs — wasmtime errors if one dir holds two package
//! declarations. The synthetic kernel world is supplied via the `inline:`
//! option in `bindings.rs`.
//!
//! No external WIT packages are vendored — the host ABI is fully
//! Astrid-owned (`astrid:*` only, no `wasi:*` dependency).
Expand Down Expand Up @@ -89,9 +93,12 @@ fn stage_wit() {
continue;
}
let stem = file_name.trim_end_matches(".wit");
let pkg_name = stem.split('@').next().unwrap();
let dst_dir = deps.join(format!("astrid-{pkg_name}"));
fs::create_dir_all(&dst_dir).expect("mkdir deps/astrid-<pkg>");
// Key the dest dir by the FULL `<pkg>@<ver>` stem, not just the package
// name. Two versions of one package (`http@1.0.0`, `http@1.1.0`) must
// land in separate `deps/` dirs — wasmtime's WIT resolver rejects a dir
// that contains two distinct package declarations.
let dst_dir = deps.join(format!("astrid-{stem}"));
fs::create_dir_all(&dst_dir).expect("mkdir deps/astrid-<pkg>@<ver>");
let dst = dst_dir.join(file_name);
fs::copy(&path, &dst).expect("copy host wit");
println!("cargo:rerun-if-changed={}", path.display());
Expand Down
32 changes: 26 additions & 6 deletions crates/astrid-capsule/src/engine/wasm/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@
//! for the host packages it imports — no toolchain-stubbed exports show
//! up at runtime.
//!
//! Note: every package is pinned at `@1.0.0`. When a new frozen version
//! ships (e.g. `host/ipc@1.1.0.wit`), add it here as an additional import
//! AND register a second `add_to_linker` call — the wasmtime Component
//! Model linker enforces exact `(package, version)` matches, so multiple
//! versions must be registered explicitly to allow old and new capsules
//! to coexist.
//! Multi-version coexistence: most packages are pinned at `@1.0.0`, but a
//! frozen package can ship a successor version that lives alongside the old
//! one (e.g. `astrid:http@1.0.0` and `astrid:http@1.1.0`). To register both,
//! add ONE `import astrid:<pkg>/host@<ver>;` line to the `world kernel` below
//! for each version. The single `bindings::Kernel::add_to_linker` call in
//! `engine/wasm/mod.rs` then auto-wires every version in the world — there is
//! NO second `add_to_linker` call to add. `build.rs` stages each version into
//! its own `deps/astrid-<pkg>@<ver>/` dir so the WIT resolver sees them as
//! distinct packages. The wasmtime Component Model linker enforces exact
//! `(package, version)` matches, so an old capsule binds `@1.0.0` and a new
//! one binds `@1.1.0` off the same linker. The host trait impls for both
//! versions live on `HostState` (see `engine/wasm/host/http/mod.rs`).

wasmtime::component::bindgen!({
inline: "
Expand All @@ -52,6 +58,7 @@ wasmtime::component::bindgen!({
import astrid:kv/host@1.0.0;
import astrid:net/host@1.0.0;
import astrid:http/host@1.0.0;
import astrid:http/host@1.1.0;
import astrid:sys/host@1.0.0;
import astrid:process/host@1.0.0;
import astrid:uplink/host@1.0.0;
Expand Down Expand Up @@ -128,5 +135,18 @@ wasmtime::component::bindgen!({
"astrid:http/host.http-request": async,
"astrid:http/host.http-stream-start": async,
"astrid:http/host.[method]http-stream.read-chunk": async,
// `astrid:http@1.1.0` mirrors the @1.0.0 async set: the buffered/
// streaming entrypoints and the per-chunk read all wait on the
// network, so they `.await` reqwest directly rather than pinning a
// worker (issue #816). Selectors are package-qualified, so the
// @1.0.0 and @1.1.0 forms are distinct keys. `http-upload-start`'s
// body comes from a guest-written sink and the actual send happens
// in `http-upload.finish`, so that method is the async one for the
// upload path. The bindgen macro resolves these against whichever
// version declares the function.
"astrid:http/host@1.1.0.http-request-opts": async,
"astrid:http/host@1.1.0.http-stream-start-opts": async,
"astrid:http/host@1.1.0.[method]http-stream.read-chunk": async,
"astrid:http/host@1.1.0.[method]http-upload.finish": async,
},
});
Loading
Loading