diff --git a/Cargo.lock b/Cargo.lock index be618ca..a0cb10e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,42 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -23,12 +53,35 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auditable-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" +dependencies = [ + "semver", + "serde", + "serde_json", + "topological-sort", +] + [[package]] name = "aws-lc-rs" version = "1.17.3" @@ -78,6 +131,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytes" @@ -85,6 +141,84 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.4", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.7", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + [[package]] name = "cc" version = "1.4.0" @@ -117,7 +251,14 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "clock-probe-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", ] [[package]] @@ -129,6 +270,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "combine" version = "4.6.7" @@ -155,6 +305,13 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "counter-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -173,6 +330,144 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9e80ceb5153bb9dd0d048e685ec4df6fa20ce92d4ffffcb5d691623e1d8693" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5131015cf909d631a2afb1a3fd6bf6dbe08d896bc984a029dcbbe4a271f8dc" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" + +[[package]] +name = "cranelift-control" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add3991ccfeb20022443bae60b8adc56081f27caab0213b0ff26288954e44fe5" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc02707039d43c0e132526f1d3ac319b45468331b823a1749625825010f644e4" + +[[package]] +name = "cranelift-native" +version = "0.128.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1574664cba9100c3a25323ac0ad415c66315d86f1ed1264d887d847c350522" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" + [[package]] name = "crc32fast" version = "1.5.0" @@ -236,6 +531,40 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "echo-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -252,12 +581,36 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fault-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -289,12 +642,38 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.33" @@ -311,12 +690,34 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "futures-sink" version = "0.3.33" @@ -335,8 +736,10 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -377,10 +780,21 @@ dependencies = [ "js-sys", "libc", "r-efi", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -388,6 +802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", + "serde", ] [[package]] @@ -396,6 +811,12 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.4.2" @@ -493,6 +914,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -614,12 +1059,37 @@ dependencies = [ "serde_core", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -708,6 +1178,18 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -732,12 +1214,36 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "mime" version = "0.3.17" @@ -775,6 +1281,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -805,6 +1323,18 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -814,6 +1344,25 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -823,6 +1372,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulley-interpreter" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5893776f2df13ad218c2055c8e1ec299f4d2d2270f73d2c67fc047da81feff12" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6fe011ea26b0b2c308918c6a1d11d44f688eb89d2ac0420b91340c495b4a90" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "quinn" version = "0.11.11" @@ -853,7 +1425,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", @@ -895,6 +1467,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.2" @@ -903,7 +1486,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -918,7 +1520,21 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "regalloc2" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", ] [[package]] @@ -990,6 +1606,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -999,10 +1628,20 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.42" @@ -1130,6 +1769,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -1233,6 +1876,9 @@ name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1244,6 +1890,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + +[[package]] +name = "spin-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1298,6 +1960,28 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -1307,10 +1991,19 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "2.0.19" @@ -1419,6 +2112,12 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tower" version = "0.5.3" @@ -1471,9 +2170,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1568,6 +2279,13 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi-clock-probe-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -1623,6 +2341,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" +dependencies = [ + "leb128fmt", + "wasmparser 0.227.1", +] + [[package]] name = "wasm-encoder" version = "0.243.0" @@ -1630,7 +2358,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.243.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" +dependencies = [ + "anyhow", + "auditable-serde", + "flate2", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder 0.227.1", + "wasmparser 0.227.1", ] [[package]] @@ -1641,8 +2388,8 @@ checksum = "eae05bf9579f45a62e8d0a4e3f52eaa8da518883ac5afa482ec8256c329ecd56" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", ] [[package]] @@ -1657,11 +2404,35 @@ dependencies = [ "tempfile", "thiserror", "toml", - "wit-component", - "wit-parser", + "wit-component 0.243.0", + "wit-parser 0.243.0", "zip", ] +[[package]] +name = "wasmeld-runtime" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", + "toml", + "wasmeld-package", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "wasmparser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wasmparser" version = "0.243.0" @@ -1672,6 +2443,286 @@ dependencies = [ "hashbrown 0.15.5", "indexmap", "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.243.0", +] + +[[package]] +name = "wasmtime" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0785170265701295d3978baf927e9d97ab1ecec3b480874cf3b96cdfdce59a" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "hashbrown 0.15.5", + "indexmap", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aad0597f0ae337043554257e014a6930d9264b1a7f04db8c0730f82ec819b88" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "postcard", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca277fba7e79efe990ee464217c4e0a7fed682febfc500d446beb9a8685e1b82" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.243.0", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d805ffc49b81d6697ce57c4aaa5405714ede9aa348dde71b4bbbc1dd3dc61662" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b0e5aeaa200b69b23ee1f0ef9e301cd9d6f532fd62b0a8580b43c268ce3dd8c" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1651acaae47851714aa829e70115bffcb7048549ad35439bf1e60d6b616451" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cebd1abdfd8b3b7421cdd0b3d4a9714c540b0efc4ce31be7ca58ccc7ab195859" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe841cd2d31bd965643590fe734c3aacc647e767ab43e25ddc79b827e742fd9f" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-environ", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61fe7cfca53d0ce01dc480ce1db93ad48b6fa1f354d8ff0680ac6a76ef354a3" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475ac165f89b2733014a0e5c0aeb326b2a18bb65136d190ec0b334d976099a9" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6fcd29322d32b8783acf901c45978316c6a5e894419bb38ae0d433cf61ddc8" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b1e146d8ee191529d9be1ea246161a6cf8572e4a760eebffb039e65eaf5e8a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9dd2df08a658aaaae5d984641f4fd1f48bc563d29ecdb4afa7a571390d60cd0" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eace6cc49aa01c3117de8cf9142b7f26b7a6ffabf7367ccd13e792e139c1743d" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.243.0", +] + +[[package]] +name = "wasmtime-wasi" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d2f7ff9d1172476d401601a5de324f1ed4e7bf87f3cdd311b7a0c0bc9b1c13" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", + "system-interface", + "thiserror", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628a90f35e8a25f9d843558dcf4e6bc86dca4c7d67e21eefd16e20c17c8bc98c" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "wasmtime", ] [[package]] @@ -1712,12 +2763,85 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winch-codegen" +version = "41.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c1874c8030d004bb2cec326dac8e9e42c2b6bd1f17663512dfd9b23730336e" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -1727,6 +2851,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1812,6 +2945,98 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.227.1", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata 0.227.1", + "wit-bindgen-core", + "wit-component 0.227.1", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.227.1", + "wasm-metadata 0.227.1", + "wasmparser 0.227.1", + "wit-parser 0.227.1", +] + [[package]] name = "wit-component" version = "0.243.0" @@ -1825,10 +3050,28 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.243.0", + "wasm-metadata 0.243.0", + "wasmparser 0.243.0", + "wit-parser 0.243.0", +] + +[[package]] +name = "wit-parser" +version = "0.227.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.227.1", ] [[package]] @@ -1846,7 +3089,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.243.0", ] [[package]] @@ -1878,6 +3121,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 82d61d5..0c4b948 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,15 @@ [workspace] members = [ "crates/wasmeld-package", + "crates/wasmeld-runtime", + "components/echo", + "components/counter", + "components/fault", + "components/spin", + "components/clock-probe", + "components/wasi-clock-probe", ] -default-members = ["crates/wasmeld-package"] +default-members = ["crates/wasmeld-runtime"] resolver = "2" [workspace.package] @@ -18,6 +25,9 @@ semver = "1.0.28" sha2 = "0.10.9" thiserror = "2.0.17" toml = "0.9.8" +wasmtime = { version = "=41.0.0", default-features = false, features = ["component-model", "cranelift", "runtime", "std"] } +wasmtime-wasi = { version = "=41.0.0", default-features = false, features = ["p2"] } +wit-bindgen = "=0.41.0" wit-component = "=0.243.0" wit-parser = "0.243.0" zip = { version = "2.4.2", default-features = false, features = ["deflate"] } diff --git a/components/clock-probe/Cargo.toml b/components/clock-probe/Cargo.toml new file mode 100644 index 0000000..3454616 --- /dev/null +++ b/components/clock-probe/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "clock-probe-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "clock-probe" +world = "component:clock-probe/clock-probe-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/clock-probe/src/lib.rs b/components/clock-probe/src/lib.rs new file mode 100644 index 0000000..33d7bda --- /dev/null +++ b/components/clock-probe/src/lib.rs @@ -0,0 +1,24 @@ +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "clock-probe-component", + generate_all, + }); +} + +struct ClockProbe; + +impl bindings::Guest for ClockProbe { + fn init(_config: Vec) -> Result<(), bindings::ServiceError> { + Ok(()) + } + + fn invoke(input: Vec) -> Result, bindings::ServiceError> { + let now = bindings::wasmeld::clock::monotonic_clock::now(); + let mut output = now.to_le_bytes().to_vec(); + output.extend_from_slice(&input); + Ok(output) + } +} + +bindings::export!(ClockProbe with_types_in bindings); diff --git a/components/clock-probe/wasmeld.toml b/components/clock-probe/wasmeld.toml new file mode 100644 index 0000000..0d1cac9 --- /dev/null +++ b/components/clock-probe/wasmeld.toml @@ -0,0 +1,11 @@ +schema_version = 1 + +[dependencies] +"wasmeld:clock" = "0.1.0" +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:clock"] +path = "../../wit/clock" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/clock-probe/wit.lock b/components/clock-probe/wit.lock new file mode 100644 index 0000000..e7753ec --- /dev/null +++ b/components/clock-probe/wit.lock @@ -0,0 +1,15 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:clock" +version = "0.1.0" +source = "path+../../wit/clock" +sha256 = "93079c69d1b9cb3afc28da6fe4a7e37f724d0a09e780c847f8524d3920a90960" +replaced = true + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/clock-probe/wit/world.wit b/components/clock-probe/wit/world.wit new file mode 100644 index 0000000..06ec11f --- /dev/null +++ b/components/clock-probe/wit/world.wit @@ -0,0 +1,6 @@ +package component:clock-probe@0.1.0; + +world clock-probe-component { + include wasmeld:service/service-component@0.1.0; + import wasmeld:clock/monotonic-clock@0.1.0; +} diff --git a/components/counter/Cargo.toml b/components/counter/Cargo.toml new file mode 100644 index 0000000..14111e8 --- /dev/null +++ b/components/counter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "counter-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "counter" +world = "component:counter/counter-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/counter/src/lib.rs b/components/counter/src/lib.rs new file mode 100644 index 0000000..a9bac5b --- /dev/null +++ b/components/counter/src/lib.rs @@ -0,0 +1,28 @@ +use core::sync::atomic::{AtomicU64, Ordering}; + +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "counter-component", + }); +} + +static COUNTER: AtomicU64 = AtomicU64::new(0); + +struct Counter; + +impl bindings::Guest for Counter { + fn init(_config: Vec) -> Result<(), bindings::ServiceError> { + // Calls are serialized by the host Actor, so this component intentionally + // uses one mutable global to make resident memory observable in tests. + COUNTER.store(0, Ordering::Relaxed); + Ok(()) + } + + fn invoke(_input: Vec) -> Result, bindings::ServiceError> { + let next = COUNTER.fetch_add(1, Ordering::Relaxed).wrapping_add(1); + Ok(next.to_le_bytes().to_vec()) + } +} + +bindings::export!(Counter with_types_in bindings); diff --git a/components/counter/wasmeld.toml b/components/counter/wasmeld.toml new file mode 100644 index 0000000..e9165c5 --- /dev/null +++ b/components/counter/wasmeld.toml @@ -0,0 +1,7 @@ +schema_version = 1 + +[dependencies] +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/counter/wit.lock b/components/counter/wit.lock new file mode 100644 index 0000000..128b39a --- /dev/null +++ b/components/counter/wit.lock @@ -0,0 +1,8 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/counter/wit/world.wit b/components/counter/wit/world.wit new file mode 100644 index 0000000..0392217 --- /dev/null +++ b/components/counter/wit/world.wit @@ -0,0 +1,5 @@ +package component:counter@0.1.0; + +world counter-component { + include wasmeld:service/service-component@0.1.0; +} diff --git a/components/echo/Cargo.toml b/components/echo/Cargo.toml new file mode 100644 index 0000000..ce174e1 --- /dev/null +++ b/components/echo/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "echo-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "echo" +world = "component:echo/echo-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/echo/src/lib.rs b/components/echo/src/lib.rs new file mode 100644 index 0000000..134ccae --- /dev/null +++ b/components/echo/src/lib.rs @@ -0,0 +1,20 @@ +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "echo-component", + }); +} + +struct Echo; + +impl bindings::Guest for Echo { + fn init(_config: Vec) -> Result<(), bindings::ServiceError> { + Ok(()) + } + + fn invoke(input: Vec) -> Result, bindings::ServiceError> { + Ok(input) + } +} + +bindings::export!(Echo with_types_in bindings); diff --git a/components/echo/wasmeld.toml b/components/echo/wasmeld.toml new file mode 100644 index 0000000..e9165c5 --- /dev/null +++ b/components/echo/wasmeld.toml @@ -0,0 +1,7 @@ +schema_version = 1 + +[dependencies] +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/echo/wit.lock b/components/echo/wit.lock new file mode 100644 index 0000000..128b39a --- /dev/null +++ b/components/echo/wit.lock @@ -0,0 +1,8 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/echo/wit/world.wit b/components/echo/wit/world.wit new file mode 100644 index 0000000..72ab42f --- /dev/null +++ b/components/echo/wit/world.wit @@ -0,0 +1,5 @@ +package component:echo@0.1.0; + +world echo-component { + include wasmeld:service/service-component@0.1.0; +} diff --git a/components/fault/Cargo.toml b/components/fault/Cargo.toml new file mode 100644 index 0000000..47ba83f --- /dev/null +++ b/components/fault/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fault-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "fault" +world = "component:fault/fault-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/fault/src/lib.rs b/components/fault/src/lib.rs new file mode 100644 index 0000000..de2ddf1 --- /dev/null +++ b/components/fault/src/lib.rs @@ -0,0 +1,21 @@ +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "fault-component", + }); +} + +struct Fault; + +impl bindings::Guest for Fault { + fn init(_config: Vec) -> Result<(), bindings::ServiceError> { + Ok(()) + } + + fn invoke(input: Vec) -> Result, bindings::ServiceError> { + assert_ne!(input, b"fault", "intentional test fault"); + Ok(input) + } +} + +bindings::export!(Fault with_types_in bindings); diff --git a/components/fault/wasmeld.toml b/components/fault/wasmeld.toml new file mode 100644 index 0000000..e9165c5 --- /dev/null +++ b/components/fault/wasmeld.toml @@ -0,0 +1,7 @@ +schema_version = 1 + +[dependencies] +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/fault/wit.lock b/components/fault/wit.lock new file mode 100644 index 0000000..128b39a --- /dev/null +++ b/components/fault/wit.lock @@ -0,0 +1,8 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/fault/wit/world.wit b/components/fault/wit/world.wit new file mode 100644 index 0000000..eb38cb3 --- /dev/null +++ b/components/fault/wit/world.wit @@ -0,0 +1,5 @@ +package component:fault@0.1.0; + +world fault-component { + include wasmeld:service/service-component@0.1.0; +} diff --git a/components/spin/Cargo.toml b/components/spin/Cargo.toml new file mode 100644 index 0000000..1d6a8cd --- /dev/null +++ b/components/spin/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "spin-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "spin" +world = "component:spin/spin-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/spin/src/lib.rs b/components/spin/src/lib.rs new file mode 100644 index 0000000..a328b3c --- /dev/null +++ b/components/spin/src/lib.rs @@ -0,0 +1,43 @@ +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "spin-component", + }); +} + +static mut SPIN_STATE: u64 = 0; + +struct Spin; + +impl bindings::Guest for Spin { + fn init(config: Vec) -> Result<(), bindings::ServiceError> { + if config == b"spin" { + burn(u64::MAX); + } + + Ok(()) + } + + fn invoke(input: Vec) -> Result, bindings::ServiceError> { + let steps = input + .get(..size_of::()) + .and_then(|bytes| bytes.try_into().ok()) + .map(u64::from_le_bytes) + .unwrap_or_default(); + + burn(steps); + Ok(Vec::new()) + } +} + +// The volatile write makes every loop iteration observable to the optimizer. +fn burn(steps: u64) { + let mut state = unsafe { core::ptr::read_volatile(&raw const SPIN_STATE) }; + + for _ in 0..steps { + state = state.rotate_left(7).wrapping_add(0x9e37_79b9_7f4a_7c15); + unsafe { core::ptr::write_volatile(&raw mut SPIN_STATE, state) }; + } +} + +bindings::export!(Spin with_types_in bindings); diff --git a/components/spin/wasmeld.toml b/components/spin/wasmeld.toml new file mode 100644 index 0000000..e9165c5 --- /dev/null +++ b/components/spin/wasmeld.toml @@ -0,0 +1,7 @@ +schema_version = 1 + +[dependencies] +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/spin/wit.lock b/components/spin/wit.lock new file mode 100644 index 0000000..128b39a --- /dev/null +++ b/components/spin/wit.lock @@ -0,0 +1,8 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/spin/wit/world.wit b/components/spin/wit/world.wit new file mode 100644 index 0000000..de498fc --- /dev/null +++ b/components/spin/wit/world.wit @@ -0,0 +1,5 @@ +package component:spin@0.1.0; + +world spin-component { + include wasmeld:service/service-component@0.1.0; +} diff --git a/components/wasi-clock-probe/Cargo.toml b/components/wasi-clock-probe/Cargo.toml new file mode 100644 index 0000000..09a48f7 --- /dev/null +++ b/components/wasi-clock-probe/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasi-clock-probe-component" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[package.metadata.wasmeld] +id = "wasi-clock-probe" +world = "component:wasi-clock-probe/wasi-clock-probe-component@0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen.workspace = true diff --git a/components/wasi-clock-probe/src/lib.rs b/components/wasi-clock-probe/src/lib.rs new file mode 100644 index 0000000..c7e1cd5 --- /dev/null +++ b/components/wasi-clock-probe/src/lib.rs @@ -0,0 +1,21 @@ +mod bindings { + wit_bindgen::generate!({ + path: "wit", + world: "wasi-clock-probe-component", + }); +} + +struct WasiClockProbe; + +impl bindings::Guest for WasiClockProbe { + fn init(_config: Vec) -> Result<(), bindings::ServiceError> { + Ok(()) + } + + fn invoke(input: Vec) -> Result, bindings::ServiceError> { + let _ = std::time::Instant::now(); + Ok(input) + } +} + +bindings::export!(WasiClockProbe with_types_in bindings); diff --git a/components/wasi-clock-probe/wasmeld.toml b/components/wasi-clock-probe/wasmeld.toml new file mode 100644 index 0000000..e9165c5 --- /dev/null +++ b/components/wasi-clock-probe/wasmeld.toml @@ -0,0 +1,7 @@ +schema_version = 1 + +[dependencies] +"wasmeld:service" = "0.1.0" + +[replace."wasmeld:service"] +path = "../../wit/service" diff --git a/components/wasi-clock-probe/wit.lock b/components/wasi-clock-probe/wit.lock new file mode 100644 index 0000000..128b39a --- /dev/null +++ b/components/wasi-clock-probe/wit.lock @@ -0,0 +1,8 @@ +schema_version = 1 + +[[package]] +name = "wasmeld:service" +version = "0.1.0" +source = "path+../../wit/service" +sha256 = "d5497307bbcd1e159f7707f385b488a6f2e26362d5256d5bc1080ac603a51305" +replaced = true diff --git a/components/wasi-clock-probe/wit/world.wit b/components/wasi-clock-probe/wit/world.wit new file mode 100644 index 0000000..3f2475b --- /dev/null +++ b/components/wasi-clock-probe/wit/world.wit @@ -0,0 +1,5 @@ +package component:wasi-clock-probe@0.1.0; + +world wasi-clock-probe-component { + include wasmeld:service/service-component@0.1.0; +} diff --git a/crates/wasmeld-runtime/Cargo.toml b/crates/wasmeld-runtime/Cargo.toml new file mode 100644 index 0000000..018ae81 --- /dev/null +++ b/crates/wasmeld-runtime/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasmeld-runtime" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true +toml.workspace = true +wasmeld-package = { path = "../wasmeld-package" } +wasmtime.workspace = true +wasmtime-wasi.workspace = true + +[dev-dependencies] diff --git a/crates/wasmeld-runtime/src/bindings.rs b/crates/wasmeld-runtime/src/bindings.rs new file mode 100644 index 0000000..012a19d --- /dev/null +++ b/crates/wasmeld-runtime/src/bindings.rs @@ -0,0 +1,16 @@ +//! Typed host and guest bindings generated from independently versioned WIT packages. + +// The service world defines the exports every runnable component implements. +wasmtime::component::bindgen!({ + path: "../../wit/service", + world: "service-component", +}); + +pub(crate) mod clock { + // Host capabilities are generated separately so adding a capability does + // not expand the baseline service contract for every component. + wasmtime::component::bindgen!({ + path: "../../wit/clock", + world: "clock-host", + }); +} diff --git a/crates/wasmeld-runtime/src/error.rs b/crates/wasmeld-runtime/src/error.rs new file mode 100644 index 0000000..b3d7661 --- /dev/null +++ b/crates/wasmeld-runtime/src/error.rs @@ -0,0 +1,81 @@ +//! Runtime error taxonomy exposed to management layers. + +use std::time::Duration; + +use thiserror::Error; + +use crate::manifest::ServiceKey; + +/// Failures from Runtime configuration, sandbox validation, or Actor execution. +#[derive(Debug, Error)] +pub enum RuntimeError { + #[error("service id and revision must not be empty")] + InvalidServiceKey, + + #[error("invalid manifest: {0}")] + InvalidManifest(String), + + #[error("manifest parse failed: {0}")] + ManifestParse(#[from] toml::de::Error), + + #[error("service revision {0} is not registered")] + ServiceNotRegistered(ServiceKey), + + #[error("service revision {0} is already registered")] + ServiceAlreadyRegistered(ServiceKey), + + #[error("component compilation failed: {0}")] + ComponentCompilation(#[source] wasmtime::Error), + + #[error("component imports unsupported capability {0}")] + UnsupportedImport(String), + + #[error("runtime creation failed: {0}")] + RuntimeCreation(#[source] wasmtime::Error), + + #[error("failed to read component artifact {path}: {source}")] + ArtifactRead { + path: String, + #[source] + source: std::io::Error, + }, + + #[error("failed to spawn actor thread: {0}")] + ActorThread(#[source] std::io::Error), + + #[error("actor initialization failed: {0}")] + ActorInitialization(String), + + #[error("actor for {0} is unavailable")] + ActorUnavailable(ServiceKey), + + #[error("actor mailbox for {0} is full")] + ActorOverloaded(ServiceKey), + + #[error("actor for {0} stopped before returning a result")] + ActorStopped(ServiceKey), + + #[error("input for {service} exceeds the {limit}-byte limit")] + InputTooLarge { service: ServiceKey, limit: usize }, + + #[error("output for {service} exceeds the {limit}-byte limit")] + OutputTooLarge { service: ServiceKey, limit: usize }, + + #[error("actor for {service} exceeded its {deadline:?} execution deadline")] + DeadlineExceeded { + service: ServiceKey, + deadline: Duration, + }, + + #[error("actor for {service} faulted: {message}")] + ActorFault { + service: ServiceKey, + message: String, + }, + + #[error("component returned {kind}: {message}")] + ComponentError { kind: &'static str, message: String }, + + #[error("runtime lock was poisoned")] + LockPoisoned, +} diff --git a/crates/wasmeld-runtime/src/lib.rs b/crates/wasmeld-runtime/src/lib.rs new file mode 100644 index 0000000..2667d3b --- /dev/null +++ b/crates/wasmeld-runtime/src/lib.rs @@ -0,0 +1,16 @@ +//! A minimal resident WebAssembly Component runtime. +//! +//! The runtime intentionally has no network protocol or business capabilities. +//! It provides a restricted WASI compatibility context, links explicitly +//! imported host capabilities, loads a Component that implements the stable +//! service exports, and invokes it through one serial Actor. + +mod bindings; +mod error; +mod manifest; +mod runtime; + +pub use error::RuntimeError; +pub use manifest::{ResourceLimits, ServiceKey, ServiceManifest}; +pub use runtime::{ActorHandle, Runtime, RuntimeConfig, RuntimeStats}; +pub use wasmeld_package::SERVICE_WORLD; diff --git a/crates/wasmeld-runtime/src/manifest.rs b/crates/wasmeld-runtime/src/manifest.rs new file mode 100644 index 0000000..0719a59 --- /dev/null +++ b/crates/wasmeld-runtime/src/manifest.rs @@ -0,0 +1,161 @@ +//! Runtime-owned service identity and resource policy. + +use std::{fmt, time::Duration}; + +use serde::{Deserialize, Serialize}; + +use crate::error::RuntimeError; + +/// Immutable identity of one service revision. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct ServiceKey { + id: String, + revision: String, +} + +impl ServiceKey { + /// Creates a non-empty service identity. + pub fn new(id: impl Into, revision: impl Into) -> Result { + let id = id.into(); + let revision = revision.into(); + + if id.trim().is_empty() || revision.trim().is_empty() { + return Err(RuntimeError::InvalidServiceKey); + } + + Ok(Self { id, revision }) + } + + /// Returns the stable service identifier. + pub fn id(&self) -> &str { + &self.id + } + + /// Returns the immutable service revision. + pub fn revision(&self) -> &str { + &self.revision + } +} + +impl fmt::Display for ServiceKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}@{}", self.id, self.revision) + } +} + +/// Runtime manifest generated by the Console for one registered component. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ServiceManifest { + /// Stable service identifier. + pub id: String, + /// Immutable component revision. + pub revision: String, + /// Local path to the compiled Component. + pub component: String, + /// Fully versioned WIT world implemented by the Component. + pub world: String, + /// Platform-owned execution and I/O limits. + pub limits: ResourceLimits, +} + +impl ServiceManifest { + /// Parses and validates a TOML runtime manifest. + pub fn from_toml(source: &str) -> Result { + let manifest = toml::from_str::(source)?; + manifest.validate()?; + Ok(manifest) + } + + /// Derives the map key used by Runtime and Console state. + pub fn key(&self) -> Result { + ServiceKey::new(self.id.clone(), self.revision.clone()) + } + + /// Validates component path, WIT world, identity, and resource limits. + pub fn validate(&self) -> Result<(), RuntimeError> { + self.key()?; + + if self.component.trim().is_empty() { + return Err(RuntimeError::InvalidManifest( + "component must not be empty".to_owned(), + )); + } + + wasmeld_package::validate_world(&self.world) + .map_err(|error| RuntimeError::InvalidManifest(error.to_string()))?; + + self.limits.validate() + } +} + +/// Per-Actor sandbox, scheduling, mailbox, and payload limits. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ResourceLimits { + /// Maximum linear memory available to the Store. + pub memory_bytes: usize, + /// Wasmtime fuel replenished before each component call. + pub fuel_per_call: u64, + /// End-to-end deadline for one initialization or invocation. + pub deadline_ms: u64, + /// Number of queued commands accepted by the bounded Actor mailbox. + pub mailbox_capacity: usize, + /// Maximum request payload size. + pub max_input_bytes: usize, + /// Maximum response payload size. + pub max_output_bytes: usize, +} + +impl Default for ResourceLimits { + fn default() -> Self { + Self { + memory_bytes: 64 * 1024 * 1024, + fuel_per_call: 1_000_000, + deadline_ms: 500, + mailbox_capacity: 16, + max_input_bytes: 16 * 1024, + max_output_bytes: 16 * 1024, + } + } +} + +impl ResourceLimits { + /// Rejects limits that cannot enforce a meaningful execution boundary. + pub fn validate(&self) -> Result<(), RuntimeError> { + if self.memory_bytes == 0 { + return Err(RuntimeError::InvalidManifest( + "limits.memory_bytes must be greater than zero".to_owned(), + )); + } + + if self.fuel_per_call == 0 { + return Err(RuntimeError::InvalidManifest( + "limits.fuel_per_call must be greater than zero".to_owned(), + )); + } + + if self.deadline_ms == 0 { + return Err(RuntimeError::InvalidManifest( + "limits.deadline_ms must be greater than zero".to_owned(), + )); + } + + if self.mailbox_capacity == 0 { + return Err(RuntimeError::InvalidManifest( + "limits.mailbox_capacity must be greater than zero".to_owned(), + )); + } + + if self.max_input_bytes == 0 || self.max_output_bytes == 0 { + return Err(RuntimeError::InvalidManifest( + "input and output limits must be greater than zero".to_owned(), + )); + } + + Ok(()) + } + + /// Returns the configured call deadline. + pub fn deadline(&self) -> Duration { + Duration::from_millis(self.deadline_ms) + } +} diff --git a/crates/wasmeld-runtime/src/runtime.rs b/crates/wasmeld-runtime/src/runtime.rs new file mode 100644 index 0000000..83bcda0 --- /dev/null +++ b/crates/wasmeld-runtime/src/runtime.rs @@ -0,0 +1,931 @@ +//! Wasmtime sandbox and resident Actor lifecycle. +//! +//! Each started service revision owns one thread, Store, Component Instance, +//! and bounded synchronous mailbox. The Actor processes commands serially, so +//! component memory remains resident without allowing concurrent entry into the +//! same Store. + +use std::{ + collections::HashMap, + fs, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + mpsc::{self, Receiver, SyncSender, TrySendError}, + }, + thread, + time::{Duration, Instant}, +}; + +use wasmtime::{ + Config, Engine, Store, StoreLimits, StoreLimitsBuilder, + component::{Component, HasData, Linker, ResourceTable}, +}; +use wasmtime_wasi::{ + WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView, + cli::{WasiCli, WasiCliView as _}, + clocks::{WasiClocks, WasiClocksView as _}, + filesystem::{WasiFilesystem, WasiFilesystemView as _}, + p2::bindings::sync::{cli, clocks, filesystem, io}, +}; + +use crate::{ + RuntimeError, ServiceKey, ServiceManifest, + bindings::{ServiceComponent, ServiceError, clock as clock_bindings}, + manifest::ResourceLimits, +}; + +const DEFAULT_EPOCH_TICK: Duration = Duration::from_millis(5); +const MAX_EPOCH_TICK: Duration = Duration::from_millis(10); +const DEFAULT_MAX_WASM_STACK: usize = 512 * 1024; +const ACTOR_RESOURCE_COUNT_LIMIT: usize = 16; +const ACTOR_TABLE_ELEMENT_LIMIT: usize = 16 * 1024; +const MONOTONIC_CLOCK_IMPORT: &str = "wasmeld:clock/monotonic-clock@0.1.0"; +const ALLOWED_WASI_IMPORTS: &[&str] = &[ + "wasi:cli/environment@", + "wasi:cli/exit@", + "wasi:cli/stderr@", + "wasi:cli/stdin@", + "wasi:cli/stdout@", + "wasi:clocks/wall-clock@", + "wasi:filesystem/preopens@", + "wasi:filesystem/types@", + "wasi:io/error@", + "wasi:io/streams@", +]; + +/// Process-wide Wasmtime Engine settings. +#[derive(Clone, Debug)] +pub struct RuntimeConfig { + /// Frequency used to advance Wasmtime epoch interruption. + pub epoch_tick: Duration, + /// Maximum native stack reservation for WebAssembly execution. + pub max_wasm_stack: usize, +} + +impl Default for RuntimeConfig { + fn default() -> Self { + Self { + epoch_tick: DEFAULT_EPOCH_TICK, + max_wasm_stack: DEFAULT_MAX_WASM_STACK, + } + } +} + +impl RuntimeConfig { + fn validate(&self) -> Result<(), RuntimeError> { + if self.epoch_tick.is_zero() { + return Err(RuntimeError::InvalidManifest( + "runtime epoch_tick must be greater than zero".to_owned(), + )); + } + + if self.epoch_tick > MAX_EPOCH_TICK { + return Err(RuntimeError::InvalidManifest(format!( + "runtime epoch_tick must not exceed {MAX_EPOCH_TICK:?}" + ))); + } + + if self.max_wasm_stack == 0 { + return Err(RuntimeError::InvalidManifest( + "runtime max_wasm_stack must be greater than zero".to_owned(), + )); + } + + Ok(()) + } +} + +/// Registry and lifecycle manager for resident Component Actors. +pub struct Runtime { + inner: Arc, +} + +/// Point-in-time Runtime service and Actor counts. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RuntimeStats { + /// Number of compiled, registered service revisions. + pub registered_services: usize, + /// Number of Actors currently accepting calls. + pub running_actors: usize, +} + +struct RuntimeInner { + engine: Arc, + epoch_tick: Duration, + services: Mutex>, + actors: Mutex>, + lifecycle: Mutex<()>, + ticker: Arc, +} + +struct EpochTicker { + shutdown: mpsc::Sender<()>, + worker: Mutex>>, +} + +impl EpochTicker { + fn start(engine: Arc, tick: Duration) -> Result, RuntimeError> { + let (shutdown, receiver) = mpsc::channel(); + let worker = thread::Builder::new() + .name("wasmeld-runtime-epoch".to_owned()) + .spawn(move || { + loop { + match receiver.recv_timeout(tick) { + Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => return, + Err(mpsc::RecvTimeoutError::Timeout) => engine.increment_epoch(), + } + } + }) + .map_err(RuntimeError::ActorThread)?; + + Ok(Arc::new(Self { + shutdown, + worker: Mutex::new(Some(worker)), + })) + } +} + +impl Drop for EpochTicker { + fn drop(&mut self) { + let _ = self.shutdown.send(()); + + if let Ok(mut worker) = self.worker.lock() + && let Some(handle) = worker.take() + { + let _ = handle.join(); + } + } +} + +#[derive(Clone)] +struct RegisteredService { + manifest: ServiceManifest, + component: Arc, + capabilities: Vec, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum HostCapability { + MonotonicClock, +} + +impl Runtime { + /// Creates the shared Wasmtime Engine and epoch interruption worker. + pub fn new(config: RuntimeConfig) -> Result { + config.validate()?; + + let mut wasmtime_config = Config::new(); + wasmtime_config.wasm_component_model(true); + wasmtime_config.async_support(false); + wasmtime_config.consume_fuel(true); + wasmtime_config.epoch_interruption(true); + wasmtime_config.max_wasm_stack(config.max_wasm_stack); + + let engine = + Arc::new(Engine::new(&wasmtime_config).map_err(RuntimeError::RuntimeCreation)?); + let ticker = EpochTicker::start(Arc::clone(&engine), config.epoch_tick)?; + + Ok(Self { + inner: Arc::new(RuntimeInner { + engine, + epoch_tick: config.epoch_tick, + services: Mutex::new(HashMap::new()), + actors: Mutex::new(HashMap::new()), + lifecycle: Mutex::new(()), + ticker, + }), + }) + } + + /// Compiles and registers a Component without starting an Actor. + /// + /// Imports are checked against the restricted WASI and explicit Wasmeld + /// capability allowlist before the service becomes visible. + pub fn register( + &self, + manifest: ServiceManifest, + component_bytes: impl AsRef<[u8]>, + ) -> Result { + manifest.validate()?; + let key = manifest.key()?; + let component = Component::new(&self.inner.engine, component_bytes.as_ref()) + .map_err(RuntimeError::ComponentCompilation)?; + let capabilities = validate_component_imports(&component, &self.inner.engine)?; + + let mut services = self + .inner + .services + .lock() + .map_err(|_| RuntimeError::LockPoisoned)?; + + if services.contains_key(&key) { + return Err(RuntimeError::ServiceAlreadyRegistered(key)); + } + + services.insert( + key.clone(), + RegisteredService { + manifest, + component: Arc::new(component), + capabilities, + }, + ); + + Ok(key) + } + + /// Reads a Component from the manifest path and delegates to [`Self::register`]. + pub fn register_from_file( + &self, + manifest: ServiceManifest, + ) -> Result { + manifest.validate()?; + let component_path = manifest.component.clone(); + let bytes = fs::read(&component_path).map_err(|source| RuntimeError::ArtifactRead { + path: component_path, + source, + })?; + self.register(manifest, bytes) + } + + /// Starts or returns the resident Actor for one registered service revision. + pub fn start( + &self, + key: &ServiceKey, + init_config: Vec, + ) -> Result { + let _lifecycle = self + .inner + .lifecycle + .lock() + .map_err(|_| RuntimeError::LockPoisoned)?; + + { + let mut actors = self + .inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)?; + + if let Some(actor) = actors.get(key) { + if actor.is_available() { + return Ok(actor.clone()); + } + + if actor.is_alive() { + return Err(RuntimeError::ActorUnavailable(key.clone())); + } + } + + actors.remove(key); + } + + let service = self + .inner + .services + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .get(key) + .cloned() + .ok_or_else(|| RuntimeError::ServiceNotRegistered(key.clone()))?; + + if service.manifest.limits.deadline() < self.inner.epoch_tick { + return Err(RuntimeError::InvalidManifest(format!( + "service deadline for {key} must be at least the runtime epoch_tick" + ))); + } + + let actor = spawn_actor( + Arc::clone(&self.inner.engine), + Arc::clone(&self.inner.ticker), + self.inner.epoch_tick, + key.clone(), + service, + init_config, + )?; + + self.inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .insert(key.clone(), actor.clone()); + + Ok(actor) + } + + /// Enqueues one call on the service Actor's bounded serial mailbox. + pub fn invoke(&self, key: &ServiceKey, input: Vec) -> Result, RuntimeError> { + let actor = self + .inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .get(key) + .cloned() + .ok_or_else(|| RuntimeError::ActorUnavailable(key.clone()))?; + actor.invoke(input) + } + + /// Stops one Actor and releases its Store and Component Instance. + pub fn stop(&self, key: &ServiceKey) -> Result<(), RuntimeError> { + let _lifecycle = self + .inner + .lifecycle + .lock() + .map_err(|_| RuntimeError::LockPoisoned)?; + + let actor = self + .inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .get(key) + .cloned() + .ok_or_else(|| RuntimeError::ActorUnavailable(key.clone()))?; + + actor.stop()?; + self.inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .remove(key); + Ok(()) + } + + /// Registers a new revision and immediately starts its Actor. + pub fn reload( + &self, + manifest: ServiceManifest, + component_bytes: impl AsRef<[u8]>, + init_config: Vec, + ) -> Result { + let key = self.register(manifest, component_bytes)?; + self.start(&key, init_config) + } + + /// Returns counts without exposing internal Engine or Store state. + pub fn stats(&self) -> Result { + let registered_services = self + .inner + .services + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .len(); + let running_actors = self + .inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .values() + .filter(|actor| actor.is_available()) + .count(); + Ok(RuntimeStats { + registered_services, + running_actors, + }) + } + + /// Stops every Actor and returns the first stop failure, if any. + pub fn stop_all(&self) -> Result<(), RuntimeError> { + let _lifecycle = self + .inner + .lifecycle + .lock() + .map_err(|_| RuntimeError::LockPoisoned)?; + let actors = self + .inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .values() + .cloned() + .collect::>(); + let mut first_error = None; + + for actor in actors { + if !actor.is_available() { + continue; + } + if let Err(error) = actor.stop() + && first_error.is_none() + { + first_error = Some(error); + } + } + + self.inner + .actors + .lock() + .map_err(|_| RuntimeError::LockPoisoned)? + .clear(); + + match first_error { + Some(error) => Err(error), + None => Ok(()), + } + } +} + +/// Cloneable command handle for one resident Actor. +/// +/// Clones share the same bounded mailbox and status flags; they do not clone +/// the Wasmtime Store or Component Instance. +#[derive(Clone)] +pub struct ActorHandle { + key: ServiceKey, + limits: Arc, + sender: SyncSender, + status: Arc, + _ticker: Arc, +} + +impl ActorHandle { + /// Returns the service revision served by this Actor. + pub fn key(&self) -> &ServiceKey { + &self.key + } + + /// Sends an invocation and waits up to the remaining service deadline. + pub fn invoke(&self, input: Vec) -> Result, RuntimeError> { + if !self.is_available() { + return Err(RuntimeError::ActorUnavailable(self.key.clone())); + } + + if input.len() > self.limits.max_input_bytes { + return Err(RuntimeError::InputTooLarge { + service: self.key.clone(), + limit: self.limits.max_input_bytes, + }); + } + + let (response_sender, response_receiver) = mpsc::sync_channel(1); + let deadline = Instant::now() + self.limits.deadline(); + let command = ActorCommand::Invoke { + input, + deadline, + response_sender, + }; + + match self.sender.try_send(command) { + Ok(()) => {} + Err(TrySendError::Full(_)) => { + return Err(RuntimeError::ActorOverloaded(self.key.clone())); + } + Err(TrySendError::Disconnected(_)) => { + return Err(RuntimeError::ActorUnavailable(self.key.clone())); + } + } + + match response_receiver.recv_timeout(deadline.saturating_duration_since(Instant::now())) { + Ok(result) => result, + Err(mpsc::RecvTimeoutError::Timeout) => Err(RuntimeError::DeadlineExceeded { + service: self.key.clone(), + deadline: self.limits.deadline(), + }), + Err(mpsc::RecvTimeoutError::Disconnected) => { + Err(RuntimeError::ActorStopped(self.key.clone())) + } + } + } + + /// Prevents new calls, asks the worker to stop, and waits for acknowledgement. + pub fn stop(&self) -> Result<(), RuntimeError> { + if !self.status.accepting.swap(false, Ordering::AcqRel) { + return Err(RuntimeError::ActorUnavailable(self.key.clone())); + } + + let (response_sender, response_receiver) = mpsc::sync_channel(1); + match self.sender.try_send(ActorCommand::Stop { response_sender }) { + Ok(()) => {} + Err(TrySendError::Full(_)) => { + self.status.accepting.store(true, Ordering::Release); + return Err(RuntimeError::ActorOverloaded(self.key.clone())); + } + Err(TrySendError::Disconnected(_)) => { + self.status.alive.store(false, Ordering::Release); + return Err(RuntimeError::ActorUnavailable(self.key.clone())); + } + } + + response_receiver + .recv_timeout(self.limits.deadline()) + .map_err(|_| RuntimeError::ActorStopped(self.key.clone())) + } + + fn is_available(&self) -> bool { + self.status.alive.load(Ordering::Acquire) && self.status.accepting.load(Ordering::Acquire) + } + + fn is_alive(&self) -> bool { + self.status.alive.load(Ordering::Acquire) + } +} + +enum ActorCommand { + Invoke { + input: Vec, + deadline: Instant, + response_sender: SyncSender, RuntimeError>>, + }, + Stop { + response_sender: SyncSender<()>, + }, +} + +struct ActorStatus { + accepting: AtomicBool, + alive: AtomicBool, +} + +impl ActorStatus { + fn new() -> Self { + Self { + accepting: AtomicBool::new(false), + alive: AtomicBool::new(false), + } + } + + fn mark_ready(&self) { + self.alive.store(true, Ordering::Release); + self.accepting.store(true, Ordering::Release); + } + + fn mark_stopped(&self) { + self.accepting.store(false, Ordering::Release); + self.alive.store(false, Ordering::Release); + } +} + +struct HostState { + store_limits: StoreLimits, + wasi: WasiCtx, + wasi_table: ResourceTable, + clock_origin: Instant, +} + +impl HostState { + fn new(limits: &ResourceLimits) -> Self { + let store_limits = StoreLimitsBuilder::new() + .memory_size(limits.memory_bytes) + .table_elements(ACTOR_TABLE_ELEMENT_LIMIT) + .instances(ACTOR_RESOURCE_COUNT_LIMIT) + .tables(ACTOR_RESOURCE_COUNT_LIMIT) + .memories(ACTOR_RESOURCE_COUNT_LIMIT) + .trap_on_grow_failure(true) + .build(); + + let mut wasi = WasiCtxBuilder::new(); + wasi.allow_tcp(false) + .allow_udp(false) + .allow_ip_name_lookup(false) + .socket_addr_check(|_, _| Box::pin(async { false })); + + Self { + store_limits, + wasi: wasi.build(), + wasi_table: ResourceTable::new(), + clock_origin: Instant::now(), + } + } +} + +impl HasData for HostState { + type Data<'a> = &'a mut HostState; +} + +impl clock_bindings::wasmeld::clock::monotonic_clock::Host for HostState { + fn now(&mut self) -> u64 { + u64::try_from(self.clock_origin.elapsed().as_nanos()).unwrap_or(u64::MAX) + } +} + +impl WasiView for HostState { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.wasi_table, + } + } +} + +struct WasiIoTable; + +impl HasData for WasiIoTable { + type Data<'a> = &'a mut ResourceTable; +} + +struct ActorWorker { + key: ServiceKey, + limits: Arc, + epoch_tick: Duration, + store: Store, + bindings: ServiceComponent, +} + +impl ActorWorker { + fn create( + engine: Arc, + epoch_tick: Duration, + key: ServiceKey, + service: RegisteredService, + init_config: Vec, + ) -> Result { + let limits = Arc::new(service.manifest.limits.clone()); + if init_config.len() > limits.max_input_bytes { + return Err(RuntimeError::InputTooLarge { + service: key, + limit: limits.max_input_bytes, + }); + } + + let mut store = Store::new(&engine, HostState::new(&limits)); + store.limiter(|state| &mut state.store_limits); + + let mut linker = Linker::new(&engine); + add_restricted_wasi(&mut linker)?; + add_host_capabilities(&mut linker, &service.capabilities)?; + + let epoch_ticks = ticks_for(limits.deadline(), epoch_tick); + configure_call_budget(&mut store, &limits, epoch_ticks)?; + let bindings = ServiceComponent::instantiate(&mut store, &service.component, &linker) + .map_err(|error| RuntimeError::ActorInitialization(error.to_string()))?; + + configure_call_budget(&mut store, &limits, epoch_ticks)?; + + match bindings.call_init(&mut store, &init_config) { + Ok(Ok(())) => {} + Ok(Err(error)) => { + return Err(component_error("init", error)); + } + Err(error) => { + return Err(RuntimeError::ActorInitialization(error.to_string())); + } + } + + Ok(Self { + key, + limits, + epoch_tick, + store, + bindings, + }) + } + + fn invoke(&mut self, input: Vec, deadline: Instant) -> Result, RuntimeError> { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(RuntimeError::DeadlineExceeded { + service: self.key.clone(), + deadline: self.limits.deadline(), + }); + } + + configure_call_budget( + &mut self.store, + &self.limits, + ticks_for(remaining, self.epoch_tick), + )?; + + match self.bindings.call_invoke(&mut self.store, &input) { + Ok(Ok(output)) => { + if output.len() > self.limits.max_output_bytes { + return Err(RuntimeError::OutputTooLarge { + service: self.key.clone(), + limit: self.limits.max_output_bytes, + }); + } + + Ok(output) + } + Ok(Err(error)) => Err(component_error("invoke", error)), + Err(error) => Err(RuntimeError::ActorFault { + service: self.key.clone(), + message: error.to_string(), + }), + } + } +} + +fn add_restricted_wasi(linker: &mut Linker) -> Result<(), RuntimeError> { + let exit_options = cli::exit::LinkOptions::default(); + link_wasi(clocks::wall_clock::add_to_linker::( + linker, + |state| state.clocks(), + ))?; + link_wasi(filesystem::preopens::add_to_linker::< + HostState, + WasiFilesystem, + >(linker, |state| state.filesystem()))?; + link_wasi( + filesystem::types::add_to_linker::(linker, |state| { + state.filesystem() + }), + )?; + link_wasi(cli::exit::add_to_linker::( + linker, + &exit_options, + |state| state.cli(), + ))?; + link_wasi(cli::environment::add_to_linker::( + linker, + |state| state.cli(), + ))?; + link_wasi(cli::stdin::add_to_linker::( + linker, + |state| state.cli(), + ))?; + link_wasi(cli::stdout::add_to_linker::( + linker, + |state| state.cli(), + ))?; + link_wasi(cli::stderr::add_to_linker::( + linker, + |state| state.cli(), + ))?; + link_wasi(io::error::add_to_linker::( + linker, + |state| state.ctx().table, + ))?; + link_wasi(io::streams::add_to_linker::( + linker, + |state| state.ctx().table, + ))?; + Ok(()) +} + +fn link_wasi(result: wasmtime::Result<()>) -> Result<(), RuntimeError> { + result.map_err(|error| RuntimeError::ActorInitialization(error.to_string())) +} + +fn add_host_capabilities( + linker: &mut Linker, + capabilities: &[HostCapability], +) -> Result<(), RuntimeError> { + for capability in capabilities { + match capability { + HostCapability::MonotonicClock => link_wasi( + clock_bindings::wasmeld::clock::monotonic_clock::add_to_linker::< + HostState, + HostState, + >(linker, |state| state), + )?, + } + } + Ok(()) +} + +fn validate_component_imports( + component: &Component, + engine: &Engine, +) -> Result, RuntimeError> { + // Import validation is deny-by-default. A host function is linked only + // after its exact WIT identity or WASI family has passed this allowlist. + let mut capabilities = Vec::new(); + for (name, _) in component.component_type().imports(engine) { + if name == MONOTONIC_CLOCK_IMPORT { + capabilities.push(HostCapability::MonotonicClock); + } else if !ALLOWED_WASI_IMPORTS + .iter() + .any(|allowed| name.starts_with(allowed)) + { + return Err(RuntimeError::UnsupportedImport(name.to_owned())); + } + } + + Ok(capabilities) +} + +fn spawn_actor( + engine: Arc, + ticker: Arc, + epoch_tick: Duration, + key: ServiceKey, + service: RegisteredService, + init_config: Vec, +) -> Result { + let limits = Arc::new(service.manifest.limits.clone()); + let (sender, receiver) = mpsc::sync_channel(limits.mailbox_capacity); + let (ready_sender, ready_receiver) = mpsc::sync_channel(1); + let thread_key = key.clone(); + let status = Arc::new(ActorStatus::new()); + let worker_status = Arc::clone(&status); + let worker_ticker = Arc::clone(&ticker); + + thread::Builder::new() + .name(format!("wasm-actor-{thread_key}")) + .spawn(move || { + let _ticker = worker_ticker; + + match ActorWorker::create(engine, epoch_tick, key, service, init_config) { + Ok(mut worker) => { + worker_status.mark_ready(); + let _ = ready_sender.send(Ok(())); + run_actor(&mut worker, receiver, &worker_status); + } + Err(error) => { + worker_status.mark_stopped(); + let _ = ready_sender.send(Err(error)); + } + } + }) + .map_err(RuntimeError::ActorThread)?; + + ready_receiver.recv().map_err(|_| { + RuntimeError::ActorInitialization("actor thread stopped during initialization".to_owned()) + })??; + + Ok(ActorHandle { + key: thread_key, + limits, + sender, + status, + _ticker: ticker, + }) +} + +fn run_actor(worker: &mut ActorWorker, receiver: Receiver, status: &ActorStatus) { + // This is the only loop that enters the Store. Multiple ActorHandle clones + // therefore cannot invoke the same Component Instance concurrently. + while let Ok(command) = receiver.recv() { + match command { + ActorCommand::Invoke { + input, + deadline, + response_sender, + } => { + let result = if Instant::now() >= deadline { + Err(RuntimeError::DeadlineExceeded { + service: worker.key.clone(), + deadline: worker.limits.deadline(), + }) + } else { + worker.invoke(input, deadline) + }; + let fatal = is_fatal(&result); + if fatal { + status.mark_stopped(); + } + let _ = response_sender.send(result); + + if fatal { + drain_after_failure(receiver, &worker.key); + return; + } + } + ActorCommand::Stop { response_sender } => { + status.mark_stopped(); + let _ = response_sender.send(()); + return; + } + } + } + + status.mark_stopped(); +} + +fn drain_after_failure(receiver: Receiver, key: &ServiceKey) { + while let Ok(command) = receiver.try_recv() { + match command { + ActorCommand::Invoke { + response_sender, .. + } => { + let _ = response_sender.send(Err(RuntimeError::ActorUnavailable(key.clone()))); + } + ActorCommand::Stop { response_sender } => { + let _ = response_sender.send(()); + } + } + } +} + +fn configure_call_budget( + store: &mut Store, + limits: &ResourceLimits, + epoch_ticks: u64, +) -> Result<(), RuntimeError> { + store + .set_fuel(limits.fuel_per_call) + .map_err(|error| RuntimeError::ActorInitialization(error.to_string()))?; + store.set_epoch_deadline(epoch_ticks); + Ok(()) +} + +fn ticks_for(deadline: Duration, epoch_tick: Duration) -> u64 { + let deadline_nanos = deadline.as_nanos(); + let tick_nanos = epoch_tick.as_nanos().max(1); + let ticks = deadline_nanos.div_ceil(tick_nanos).max(1); + u64::try_from(ticks).unwrap_or(u64::MAX) +} + +fn component_error(kind: &'static str, error: ServiceError) -> RuntimeError { + let message = match error { + ServiceError::InitFailed(message) | ServiceError::CallFailed(message) => message, + }; + + RuntimeError::ComponentError { kind, message } +} + +fn is_fatal(result: &Result, RuntimeError>) -> bool { + matches!(result, Err(RuntimeError::ActorFault { .. })) +} diff --git a/crates/wasmeld-runtime/tests/runtime_components.rs b/crates/wasmeld-runtime/tests/runtime_components.rs new file mode 100644 index 0000000..e6cef3a --- /dev/null +++ b/crates/wasmeld-runtime/tests/runtime_components.rs @@ -0,0 +1,323 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, + sync::OnceLock, + thread, + time::{Duration, Instant}, +}; + +use wasmeld_runtime::{ResourceLimits, Runtime, RuntimeConfig, RuntimeError, ServiceManifest}; + +static COMPONENTS_BUILT: OnceLock<()> = OnceLock::new(); + +#[test] +fn echo_component_round_trips_bytes() { + let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); + let (key, actor) = start_component(&runtime, "echo", "echo_component.wasm"); + + assert_eq!(actor.invoke(b"hello wasm".to_vec()).unwrap(), b"hello wasm"); + + runtime.stop(&key).unwrap(); +} + +#[test] +fn counter_memory_is_resident_and_resets_after_restart() { + let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); + let (key, actor) = start_component(&runtime, "counter", "counter_component.wasm"); + + assert_eq!(decode_counter(actor.invoke(Vec::new()).unwrap()), 1); + assert_eq!(decode_counter(actor.invoke(Vec::new()).unwrap()), 2); + + runtime.stop(&key).unwrap(); + let restarted = runtime.start(&key, Vec::new()).unwrap(); + assert_eq!(decode_counter(restarted.invoke(Vec::new()).unwrap()), 1); + + runtime.stop(&key).unwrap(); +} + +#[test] +fn actor_serializes_concurrent_counter_calls() { + let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); + let (key, actor) = start_component(&runtime, "counter-concurrent", "counter_component.wasm"); + + let mut workers = Vec::new(); + for _ in 0..8 { + let actor = actor.clone(); + workers.push(thread::spawn(move || { + decode_counter(actor.invoke(Vec::new()).unwrap()) + })); + } + + let mut values = workers + .into_iter() + .map(|worker| worker.join().expect("worker should not panic")) + .collect::>(); + values.sort_unstable(); + assert_eq!(values, (1..=8).collect::>()); + + runtime.stop(&key).unwrap(); +} + +#[test] +fn runtime_reports_stats_and_stops_all_actors() { + let runtime = Runtime::new(RuntimeConfig::default()).unwrap(); + let (key, actor) = start_component(&runtime, "runtime-control", "echo_component.wasm"); + + assert_eq!(runtime.stats().unwrap().registered_services, 1); + assert_eq!(runtime.stats().unwrap().running_actors, 1); + + runtime.stop_all().unwrap(); + assert_eq!(runtime.stats().unwrap().registered_services, 1); + assert_eq!(runtime.stats().unwrap().running_actors, 0); + assert!(matches!( + actor.invoke(b"stopped".to_vec()), + Err(RuntimeError::ActorUnavailable(unavailable)) if unavailable == key + )); + + runtime.start(&key, Vec::new()).unwrap(); + assert_eq!( + runtime.invoke(&key, b"started-again".to_vec()).unwrap(), + b"started-again" + ); +} + +#[test] +fn faulted_actor_can_be_started_again() { + let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); + let (key, actor) = start_component(&runtime, "fault", "fault_component.wasm"); + + assert!(matches!( + actor.invoke(b"fault".to_vec()), + Err(RuntimeError::ActorFault { .. }) + )); + + let restarted = runtime.start(&key, Vec::new()).unwrap(); + assert_eq!(restarted.invoke(b"ready".to_vec()).unwrap(), b"ready"); + + runtime.stop(&key).unwrap(); +} + +#[test] +fn fuel_limit_stops_cpu_bound_component() { + let runtime = Runtime::new(RuntimeConfig { + epoch_tick: Duration::from_millis(10), + ..RuntimeConfig::default() + }) + .expect("runtime should start"); + let key = register_component( + &runtime, + "spin-fuel", + "spin_component.wasm", + ResourceLimits { + fuel_per_call: 10_000, + deadline_ms: 50, + ..test_limits() + }, + ); + let actor = runtime.start(&key, Vec::new()).unwrap(); + + let error = actor.invoke(u64::MAX.to_le_bytes().to_vec()).unwrap_err(); + assert!( + matches!(&error, RuntimeError::ActorFault { .. }), + "expected a Wasm trap after fuel exhaustion, got {error:?}" + ); + assert!(matches!( + actor.invoke(Vec::new()), + Err(RuntimeError::ActorUnavailable(_)) + )); +} + +#[test] +fn epoch_interrupts_cpu_bound_initialization() { + let runtime = Runtime::new(RuntimeConfig { + epoch_tick: Duration::from_millis(2), + ..RuntimeConfig::default() + }) + .expect("runtime should start"); + let key = register_component( + &runtime, + "spin-epoch", + "spin_component.wasm", + ResourceLimits { + fuel_per_call: 10_000_000_000, + deadline_ms: 100, + ..test_limits() + }, + ); + + let started_at = Instant::now(); + let error = match runtime.start(&key, b"spin".to_vec()) { + Ok(_) => panic!("epoch budget should interrupt the spin component"), + Err(error) => error, + }; + assert!( + matches!(&error, RuntimeError::ActorInitialization(_)), + "expected initialization to be interrupted, got {error:?}" + ); + let elapsed = started_at.elapsed(); + assert!( + elapsed >= Duration::from_millis(20), + "initialization failed before the epoch budget could expire" + ); + assert!( + elapsed < Duration::from_secs(2), + "epoch interruption took too long" + ); +} + +#[test] +fn explicit_clock_capability_is_linked() { + let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); + let (key, actor) = start_component(&runtime, "clock-probe", "clock_probe_component.wasm"); + + let first = actor.invoke(b"first".to_vec()).unwrap(); + let second = actor.invoke(b"second".to_vec()).unwrap(); + assert_eq!(&first[8..], b"first"); + assert_eq!(&second[8..], b"second"); + assert!(decode_clock(&second) >= decode_clock(&first)); + + runtime.stop(&key).unwrap(); +} + +#[test] +fn registration_rejects_non_whitelisted_wasi_imports() { + let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start"); + let artifact_name = "wasi_clock_probe_component.wasm"; + let artifact = component_artifact(artifact_name); + let manifest = ServiceManifest { + id: "wasi-clock-probe".to_owned(), + revision: "0.1.0".to_owned(), + component: artifact.display().to_string(), + world: component_world(artifact_name).to_owned(), + limits: test_limits(), + }; + + let error = runtime + .register(manifest, fs::read(artifact).unwrap()) + .expect_err("monotonic clock must not be a V1 capability"); + assert!(matches!( + error, + RuntimeError::UnsupportedImport(name) if name.starts_with("wasi:clocks/monotonic-clock@") + )); +} + +fn start_component( + runtime: &Runtime, + service_id: &str, + artifact_name: &str, +) -> (wasmeld_runtime::ServiceKey, wasmeld_runtime::ActorHandle) { + let key = register_component(runtime, service_id, artifact_name, test_limits()); + let actor = runtime.start(&key, Vec::new()).unwrap(); + (key, actor) +} + +fn register_component( + runtime: &Runtime, + service_id: &str, + artifact_name: &str, + limits: ResourceLimits, +) -> wasmeld_runtime::ServiceKey { + let artifact = component_artifact(artifact_name); + let manifest = ServiceManifest { + id: service_id.to_owned(), + revision: "0.1.0".to_owned(), + component: artifact.display().to_string(), + world: component_world(artifact_name).to_owned(), + limits, + }; + + let bytes = fs::read(&artifact).expect("component artifact should be readable"); + runtime.register(manifest, bytes).unwrap() +} + +fn component_artifact(name: &str) -> PathBuf { + build_components_once(); + workspace_root() + .join("target/wasm32-wasip2/release") + .join(name) +} + +fn build_components_once() { + COMPONENTS_BUILT.get_or_init(|| { + let toolchain = + std::env::var("WASMELD_COMPONENT_TOOLCHAIN").unwrap_or_else(|_| "1.90.0".to_owned()); + for manifest in [ + "components/echo/Cargo.toml", + "components/counter/Cargo.toml", + "components/fault/Cargo.toml", + "components/spin/Cargo.toml", + "components/clock-probe/Cargo.toml", + "components/wasi-clock-probe/Cargo.toml", + ] { + let status = Command::new("rustup") + .args([ + "run", + &toolchain, + "cargo", + "build", + "--manifest-path", + manifest, + "--target", + "wasm32-wasip2", + "--release", + ]) + .current_dir(workspace_root()) + .status() + .expect("component build command should start"); + assert!(status.success(), "component build should succeed"); + } + }); +} + +fn workspace_root() -> &'static Path { + static ROOT: OnceLock = OnceLock::new(); + ROOT.get_or_init(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("runtime crate must live in the workspace") + .to_path_buf() + }) + .as_path() +} + +fn test_limits() -> ResourceLimits { + ResourceLimits { + memory_bytes: 64 * 1024 * 1024, + fuel_per_call: 1_000_000, + deadline_ms: 500, + mailbox_capacity: 16, + max_input_bytes: 16 * 1024, + max_output_bytes: 16 * 1024, + } +} + +fn decode_counter(bytes: Vec) -> u64 { + let array: [u8; 8] = bytes + .try_into() + .expect("counter response must be a u64 payload"); + u64::from_le_bytes(array) +} + +fn decode_clock(bytes: &[u8]) -> u64 { + u64::from_le_bytes( + bytes[..8] + .try_into() + .expect("clock response must start with a u64 payload"), + ) +} + +fn component_world(artifact_name: &str) -> &'static str { + match artifact_name { + "echo_component.wasm" => "component:echo/echo-component@0.1.0", + "counter_component.wasm" => "component:counter/counter-component@0.1.0", + "fault_component.wasm" => "component:fault/fault-component@0.1.0", + "spin_component.wasm" => "component:spin/spin-component@0.1.0", + "clock_probe_component.wasm" => "component:clock-probe/clock-probe-component@0.1.0", + "wasi_clock_probe_component.wasm" => { + "component:wasi-clock-probe/wasi-clock-probe-component@0.1.0" + } + other => panic!("no WIT world registered for {other}"), + } +} diff --git a/wit/clock/package.wit b/wit/clock/package.wit new file mode 100644 index 0000000..5fd4ef8 --- /dev/null +++ b/wit/clock/package.wit @@ -0,0 +1,9 @@ +package wasmeld:clock@0.1.0; + +interface monotonic-clock { + now: func() -> u64; +} + +world clock-host { + import monotonic-clock; +} diff --git a/wit/service/package.wit b/wit/service/package.wit new file mode 100644 index 0000000..93da538 --- /dev/null +++ b/wit/service/package.wit @@ -0,0 +1,11 @@ +package wasmeld:service@0.1.0; + +world service-component { + variant service-error { + init-failed(string), + call-failed(string), + } + + export init: func(config: list) -> result<_, service-error>; + export invoke: func(input: list) -> result, service-error>; +}