merge: plugin component model runtime

This commit is contained in:
Keisuke Hirata 2026-06-20 02:20:03 +09:00
commit 63d7ad788d
No known key found for this signature in database
10 changed files with 1677 additions and 69 deletions

648
Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a"
dependencies = [
"gimli",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@ -82,6 +91,12 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "arc-swap"
version = "1.9.1"
@ -222,6 +237,9 @@ name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
dependencies = [
"allocator-api2",
]
[[package]]
name = "bytemuck"
@ -351,6 +369,15 @@ dependencies = [
"cc",
]
[[package]]
name = "cobs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
@ -422,6 +449,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpp_demangle"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253"
dependencies = [
"cfg-if",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -440,6 +476,157 @@ dependencies = [
"libc",
]
[[package]]
name = "cranelift-assembler-x64"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bc293b86236abcc45f2f72e2d18e2bd636f2a08b75eb286bae31e71e1430c91"
dependencies = [
"cranelift-assembler-x64-meta",
]
[[package]]
name = "cranelift-assembler-x64-meta"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b954c826eddaf1b001402cb8aecf1764c6f6d637ba69fb9e3311f1ebac965be6"
dependencies = [
"cranelift-srcgen",
]
[[package]]
name = "cranelift-bforest"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4053fa2575ef4a5c35d2708533df2200400ae979226cea9cc92a578b811bd4e7"
dependencies = [
"cranelift-entity",
"wasmtime-internal-core",
]
[[package]]
name = "cranelift-bitset"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d216663191014aa63e1d2cffd058e609eaf207646d40b739d88250f65b2c4f69"
dependencies = [
"serde",
"serde_derive",
"wasmtime-internal-core",
]
[[package]]
name = "cranelift-codegen"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a5e7e7aad6a425a51da1ad7ab9e5d280ea97eb7c7c4545fafb567915a75aadb"
dependencies = [
"bumpalo",
"cranelift-assembler-x64",
"cranelift-bforest",
"cranelift-bitset",
"cranelift-codegen-meta",
"cranelift-codegen-shared",
"cranelift-control",
"cranelift-entity",
"cranelift-isle",
"gimli",
"hashbrown 0.17.1",
"libm",
"log",
"pulley-interpreter",
"regalloc2",
"rustc-hash",
"serde",
"smallvec",
"target-lexicon",
"wasmtime-internal-core",
]
[[package]]
name = "cranelift-codegen-meta"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c421d80a9a85f806cb02a2983b5b5368a335c319795b1f1b4b771a24479af5b0"
dependencies = [
"cranelift-assembler-x64-meta",
"cranelift-codegen-shared",
"cranelift-srcgen",
"heck",
"pulley-interpreter",
]
[[package]]
name = "cranelift-codegen-shared"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78fdb83ab012d0ee6a44ced7ca8788a444f17cf821c62f95d6ef87c9f0262518"
[[package]]
name = "cranelift-control"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b75adc6eb7bb4ac6365106afb6cac4f12fe1ddfa02ddc9fd7015ca1469b471b"
dependencies = [
"arbitrary",
]
[[package]]
name = "cranelift-entity"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668e56db75a54816cbdd7c7b7bfc558b08bf7b2cda9d0846491517e92f3b393b"
dependencies = [
"cranelift-bitset",
"serde",
"serde_derive",
"wasmtime-internal-core",
]
[[package]]
name = "cranelift-frontend"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c63892dc1cc3ae48680183fa66997f60ffe7f1e200c8d390f8ee66edff4aef5a"
dependencies = [
"cranelift-codegen",
"log",
"smallvec",
"target-lexicon",
]
[[package]]
name = "cranelift-isle"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94eaf429c32a12715429c7c6ddfdd43c170f4cdd7e97bfa507bd68a652091087"
[[package]]
name = "cranelift-native"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd77674904ae9be11c1e1efdba54788b59f3d6658d747b97534bfbba2909aacc"
dependencies = [
"cranelift-codegen",
"libc",
"target-lexicon",
]
[[package]]
name = "cranelift-srcgen"
version = "0.132.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba7c0ff5941842c36653da155580ce41e675c204a67ac1b4e1c478a9347bbb7"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -700,6 +887,18 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[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"
@ -1027,6 +1226,18 @@ dependencies = [
"wasip3",
]
[[package]]
name = "gimli"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c"
dependencies = [
"fnv",
"hashbrown 0.16.1",
"indexmap",
"stable_deref_trait",
]
[[package]]
name = "glob"
version = "0.3.3"
@ -1122,6 +1333,17 @@ dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash 0.2.0",
"serde",
"serde_core",
]
[[package]]
name = "heck"
version = "0.5.0"
@ -1463,12 +1685,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.13.1"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
@ -1793,6 +2015,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "manifest"
version = "0.1.0"
@ -1852,6 +2083,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[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 = "memmap2"
version = "0.9.10"
@ -2077,6 +2317,18 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
[[package]]
name = "object"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
dependencies = [
"crc32fast",
"hashbrown 0.17.1",
"indexmap",
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@ -2368,6 +2620,7 @@ dependencies = [
"tracing",
"uuid",
"wasmi",
"wasmtime",
"wat",
"workflow",
]
@ -2403,6 +2656,18 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[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"
@ -2499,6 +2764,29 @@ dependencies = [
"unicase",
]
[[package]]
name = "pulley-interpreter"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d9880c1985ccccaed3646b0ef793dc39a4b117403ed4afc6fa3ef6027c5200f"
dependencies = [
"cranelift-bitset",
"log",
"pulley-macros",
"wasmtime-internal-core",
]
[[package]]
name = "pulley-macros"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee249346855ad102580e474da5463f86f8a7d449e6d49e00fefb304e448e2983"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "quinn"
version = "0.11.9"
@ -2770,6 +3058,20 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "regalloc2"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186"
dependencies = [
"allocator-api2",
"bumpalo",
"hashbrown 0.17.1",
"log",
"rustc-hash",
"smallvec",
]
[[package]]
name = "regex"
version = "1.12.3"
@ -2860,6 +3162,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]]
name = "rustc-hash"
version = "2.1.2"
@ -3091,6 +3399,10 @@ name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde"
@ -3341,6 +3653,9 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@ -3501,6 +3816,12 @@ dependencies = [
"libc",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "target-triple"
version = "1.0.0"
@ -4260,12 +4581,22 @@ dependencies = [
[[package]]
name = "wasm-encoder"
version = "0.246.2"
version = "0.248.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7"
checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6"
dependencies = [
"leb128fmt",
"wasmparser 0.246.2",
"wasmparser 0.248.0",
]
[[package]]
name = "wasm-encoder"
version = "0.252.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f"
dependencies = [
"leb128fmt",
"wasmparser 0.252.0",
]
[[package]]
@ -4357,9 +4688,22 @@ dependencies = [
[[package]]
name = "wasmparser"
version = "0.246.2"
version = "0.248.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d"
checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a"
dependencies = [
"bitflags 2.11.0",
"hashbrown 0.17.1",
"indexmap",
"semver",
"serde",
]
[[package]]
name = "wasmparser"
version = "0.252.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c"
dependencies = [
"bitflags 2.11.0",
"indexmap",
@ -4367,23 +4711,257 @@ dependencies = [
]
[[package]]
name = "wast"
version = "246.0.2"
name = "wasmprinter"
version = "0.248.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62"
checksum = "30b264a5410b008d4d199a92bf536eae703cbd614482fc1ec53831cf19e1c183"
dependencies = [
"anyhow",
"termcolor",
"wasmparser 0.248.0",
]
[[package]]
name = "wasmtime"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7ce9aa2c67f75fadcfdc6aa9097d03e7c39485dfe316f2ed6a7c0fd186c527"
dependencies = [
"addr2line",
"async-trait",
"bitflags 2.11.0",
"bumpalo",
"cc",
"cfg-if",
"encoding_rs",
"libc",
"log",
"mach2",
"memfd",
"object",
"once_cell",
"postcard",
"pulley-interpreter",
"rustix 1.1.4",
"semver",
"serde",
"serde_derive",
"smallvec",
"target-lexicon",
"wasmparser 0.248.0",
"wasmtime-environ",
"wasmtime-internal-component-macro",
"wasmtime-internal-component-util",
"wasmtime-internal-core",
"wasmtime-internal-cranelift",
"wasmtime-internal-fiber",
"wasmtime-internal-jit-debug",
"wasmtime-internal-jit-icache-coherence",
"wasmtime-internal-unwinder",
"wasmtime-internal-versioned-export-macros",
"wasmtime-internal-winch",
"windows-sys 0.61.2",
]
[[package]]
name = "wasmtime-environ"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fb157bd1fbf689ac89d570433a700db6f33bdfcb5ffc30e3f1c49e4c70de71"
dependencies = [
"anyhow",
"cpp_demangle",
"cranelift-bforest",
"cranelift-bitset",
"cranelift-entity",
"gimli",
"hashbrown 0.17.1",
"indexmap",
"log",
"object",
"postcard",
"rustc-demangle",
"semver",
"serde",
"serde_derive",
"sha2 0.10.9",
"smallvec",
"target-lexicon",
"wasm-encoder 0.248.0",
"wasmparser 0.248.0",
"wasmprinter",
"wasmtime-internal-component-util",
"wasmtime-internal-core",
]
[[package]]
name = "wasmtime-internal-component-macro"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96c17f35fae2ab574667aba0c58fd56349a6f788ac42541a2e543116d5cfb91"
dependencies = [
"anyhow",
"proc-macro2",
"quote",
"syn 2.0.117",
"wasmtime-internal-component-util",
"wasmtime-internal-wit-bindgen",
"wit-parser 0.248.0",
]
[[package]]
name = "wasmtime-internal-component-util"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2eeb9b53222859e6f5dc73d2ccfb33254d672469cac11b693a71912e2f3817"
[[package]]
name = "wasmtime-internal-core"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1deaf6bc3430abd7497b00c64f06ca2b97ca0fe41af87836446ca30949965c"
dependencies = [
"hashbrown 0.17.1",
"libm",
"serde",
]
[[package]]
name = "wasmtime-internal-cranelift"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b845f83b5b04b11bc48329b53eb4fa8cf9f28a43c71ed8e1203f68ffa9806d1b"
dependencies = [
"cfg-if",
"cranelift-codegen",
"cranelift-control",
"cranelift-entity",
"cranelift-frontend",
"cranelift-native",
"gimli",
"itertools",
"log",
"object",
"pulley-interpreter",
"smallvec",
"target-lexicon",
"thiserror 2.0.18",
"wasmparser 0.248.0",
"wasmtime-environ",
"wasmtime-internal-core",
"wasmtime-internal-unwinder",
"wasmtime-internal-versioned-export-macros",
]
[[package]]
name = "wasmtime-internal-fiber"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10c8466f72965ae85c250f90aaa7992c089a2f8502009bd0d2c9e7d6409174a"
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 = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3adfecf5621b14d8f8871f4cb4ed9f844197b1ddefc702ef4c859552cd9551"
dependencies = [
"cc",
"wasmtime-internal-versioned-export-macros",
]
[[package]]
name = "wasmtime-internal-jit-icache-coherence"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d3c1e9fb618ec45c9b3477ea683cd37bee427273d7b13bba5c66a1caaf1dd6"
dependencies = [
"cfg-if",
"libc",
"wasmtime-internal-core",
"windows-sys 0.61.2",
]
[[package]]
name = "wasmtime-internal-unwinder"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aa91132b81f1e172ec7e7c3c114ac34209ee6b3524b3a8d6943af99803f66c5"
dependencies = [
"cfg-if",
"cranelift-codegen",
"log",
"object",
"wasmtime-environ",
]
[[package]]
name = "wasmtime-internal-versioned-export-macros"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea811ffe23f597cc7708327ea25d9eb018dcf760ffe15ccb7d0b27ad635de61"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "wasmtime-internal-winch"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "828b66175c54a0d00b4c1c1c76658d8aa73aeb9fa3553575c5eee56d40f2eb18"
dependencies = [
"cranelift-codegen",
"gimli",
"log",
"object",
"target-lexicon",
"wasmparser 0.248.0",
"wasmtime-environ",
"wasmtime-internal-cranelift",
"winch-codegen",
]
[[package]]
name = "wasmtime-internal-wit-bindgen"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae00896ad9bef1b3ca6401ae9a841daa6f357dd91541b6baf87082946d1bde1"
dependencies = [
"anyhow",
"bitflags 2.11.0",
"heck",
"indexmap",
"wit-parser 0.248.0",
]
[[package]]
name = "wast"
version = "252.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65"
dependencies = [
"bumpalo",
"leb128fmt",
"memchr",
"unicode-width",
"wasm-encoder 0.246.2",
"wasm-encoder 0.252.0",
]
[[package]]
name = "wat"
version = "1.246.2"
version = "1.252.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c"
checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb"
dependencies = [
"wast",
]
@ -4529,6 +5107,25 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "winch-codegen"
version = "45.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89c09acfdfa281b3340e1e94ef3cf6618d69eab975280f881e154c29f49419c1"
dependencies = [
"cranelift-assembler-x64",
"cranelift-codegen",
"gimli",
"regalloc2",
"smallvec",
"target-lexicon",
"thiserror 2.0.18",
"wasmparser 0.248.0",
"wasmtime-environ",
"wasmtime-internal-core",
"wasmtime-internal-cranelift",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@ -4802,7 +5399,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
"wit-parser 0.244.0",
]
[[package]]
@ -4852,7 +5449,7 @@ dependencies = [
"wasm-encoder 0.244.0",
"wasm-metadata",
"wasmparser 0.244.0",
"wit-parser",
"wit-parser 0.244.0",
]
[[package]]
@ -4873,6 +5470,25 @@ dependencies = [
"wasmparser 0.244.0",
]
[[package]]
name = "wit-parser"
version = "0.248.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "247ad505da2915a082fe13204c5ba8788425aea1de54f43b284818cf82637856"
dependencies = [
"anyhow",
"hashbrown 0.17.1",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser 0.248.0",
]
[[package]]
name = "workflow"
version = "0.1.0"

View File

@ -378,12 +378,28 @@ impl PluginPackageManifest {
}
}
pub const PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
pub const PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
/// Manifest runtime kind for WebAssembly Component Model Tool packages.
///
/// Component runtime manifests must set `component` to the packaged component
/// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`]. Raw core-Wasm
/// packages remain explicit `kind = "wasm"` plus `abi = "yoi-plugin-wasm-1"`.
pub const PLUGIN_RUNTIME_COMPONENT_KIND: &str = "wasm-component";
pub const PLUGIN_COMPONENT_TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PluginRuntimeManifest {
pub kind: String,
pub entry: String,
#[serde(default)]
pub entry: Option<String>,
#[serde(default)]
pub abi: Option<String>,
#[serde(default)]
pub component: Option<String>,
#[serde(default)]
pub world: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -837,7 +853,7 @@ pub fn read_resolved_plugin_runtime_module(
.with_digest(&record.digest)
})?;
if runtime.kind != "wasm" {
if runtime.kind != PLUGIN_RUNTIME_WASM_KIND {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
@ -848,7 +864,7 @@ pub fn read_resolved_plugin_runtime_module(
.with_package(&record.package_label)
.with_digest(&record.digest));
}
if runtime.abi.as_deref() != Some("yoi-plugin-wasm-1") {
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
@ -860,6 +876,18 @@ pub fn read_resolved_plugin_runtime_module(
.with_digest(&record.digest));
}
let entry = runtime.entry.as_deref().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin WASM runtime entry is required",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
let metadata = fs::metadata(&record.package_path).map_err(|error| {
PluginDiagnostic::new(
PluginDiagnosticKind::Io,
@ -926,13 +954,13 @@ pub fn read_resolved_plugin_runtime_module(
}
validate_manifest_path(
&runtime.entry,
entry,
&archive,
&record.package_label,
record.source,
&record.manifest.id,
)?;
let normalized = normalize_archive_path(&runtime.entry).ok_or_else(|| {
let normalized = normalize_archive_path(entry).ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Traversal,
PluginDiagnosticPhase::Manifest,
@ -956,6 +984,154 @@ pub fn read_resolved_plugin_runtime_module(
})
}
/// Reads the WebAssembly Component Model artifact selected by a resolved plugin
/// package manifest while preserving package digest pinning.
pub fn read_resolved_plugin_runtime_component(
record: &ResolvedPluginRecord,
limits: &PluginDiscoveryLimits,
) -> Result<Vec<u8>, PluginDiagnostic> {
let runtime = record.manifest.runtime.as_ref().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"resolved plugin package does not declare a component runtime",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
if runtime.kind != PLUGIN_RUNTIME_COMPONENT_KIND {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin runtime kind is unsupported",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
if runtime.world.as_deref() != Some(PLUGIN_COMPONENT_TOOL_WORLD) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin component world is unsupported",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
let component = runtime.component.as_deref().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin component runtime artifact is required",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
let metadata = fs::metadata(&record.package_path).map_err(|error| {
PluginDiagnostic::new(
PluginDiagnosticKind::Io,
PluginDiagnosticPhase::Discovery,
format!(
"resolved plugin package metadata could not be read: {}",
safe_io_error(&error)
),
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
if !metadata.is_file() {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Malformed,
PluginDiagnosticPhase::Discovery,
"resolved plugin package is not a regular file",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
if metadata.len() > limits.max_package_size_bytes {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Bounds,
PluginDiagnosticPhase::Discovery,
"resolved plugin package exceeds the configured package size bound",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
let bytes = fs::read(&record.package_path).map_err(|error| {
PluginDiagnostic::new(
PluginDiagnosticKind::Io,
PluginDiagnosticPhase::Discovery,
format!(
"resolved plugin package content could not be read: {}",
safe_io_error(&error)
),
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
let archive = parse_stored_zip(&bytes, &record.package_label, record.source, limits)?;
let actual_digest = deterministic_digest(&archive.files);
if !digest_matches(&record.digest, &actual_digest) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Digest,
PluginDiagnosticPhase::Resolution,
"resolved plugin package digest does not match current package content",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(actual_digest));
}
validate_manifest_path(
component,
&archive,
&record.package_label,
record.source,
&record.manifest.id,
)?;
let normalized = normalize_archive_path(component).ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Traversal,
PluginDiagnosticPhase::Manifest,
"plugin manifest references a path outside the package root",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
archive.files.get(&normalized).cloned().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin runtime component artifact is missing from the package",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})
}
#[derive(Clone, Debug)]
struct PluginStore {
source: PluginSourceKind,
@ -1237,17 +1413,9 @@ fn validate_manifest(
.with_package(label));
}
if let Some(runtime) = &manifest.runtime {
if runtime.kind != "wasm" {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin runtime kind is unsupported",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
if runtime.abi.as_deref() != Some("yoi-plugin-wasm-1") {
match runtime.kind.as_str() {
PLUGIN_RUNTIME_WASM_KIND => {
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
@ -1257,7 +1425,72 @@ fn validate_manifest(
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
validate_manifest_path(&runtime.entry, archive, label, source, &manifest.id)?;
let Some(entry) = runtime.entry.as_deref() else {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin WASM runtime entry is required",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
};
if runtime.component.is_some() || runtime.world.is_some() {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Malformed,
PluginDiagnosticPhase::Manifest,
"plugin WASM runtime must not declare component metadata",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
validate_manifest_path(entry, archive, label, source, &manifest.id)?;
}
PLUGIN_RUNTIME_COMPONENT_KIND => {
if runtime.abi.is_some() || runtime.entry.is_some() {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Malformed,
PluginDiagnosticPhase::Manifest,
"plugin component runtime must not declare raw WASM ABI metadata",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
if runtime.world.as_deref() != Some(PLUGIN_COMPONENT_TOOL_WORLD) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin component world is unsupported",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
let Some(component) = runtime.component.as_deref() else {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin component runtime artifact is required",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
};
validate_manifest_path(component, archive, label, source, &manifest.id)?;
}
_ => {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin runtime kind is unsupported",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
}
}
for hook in &manifest.hooks {
if !is_safe_id(&hook.id) {

View File

@ -37,6 +37,7 @@ uuid = { workspace = true, features = ["v7"] }
session-metrics = { workspace = true }
arc-swap = "1.9.1"
wasmi = { version = "0.51.1", default-features = false, features = ["std", "extra-checks"] }
wasmtime = { version = "45.0.2", default-features = false, features = ["std", "runtime", "cranelift", "component-model"] }
[dev-dependencies]
dotenv = "0.15.0"

View File

@ -21,8 +21,10 @@ use llm_worker::tool::{
Tool, ToolDefinition, ToolError, ToolExecutionContext, ToolMeta, ToolOrigin, ToolOutput,
};
use manifest::plugin::{
PluginConfig, PluginDiscoveryLimits, PluginFsGrant, PluginFsOperation, PluginHostApi,
PluginPermission, PluginSurface, PluginToolManifest, ResolvedPluginRecord,
PLUGIN_COMPONENT_TOOL_WORLD, PLUGIN_RUNTIME_COMPONENT_KIND, PLUGIN_RUNTIME_WASM_ABI,
PLUGIN_RUNTIME_WASM_KIND, PluginConfig, PluginDiscoveryLimits, PluginFsGrant,
PluginFsOperation, PluginHostApi, PluginPermission, PluginSurface, PluginToolManifest,
ResolvedPluginRecord, read_resolved_plugin_runtime_component,
read_resolved_plugin_runtime_module,
};
use serde::{Deserialize, Serialize};
@ -134,26 +136,51 @@ pub struct PluginToolEligibility {
pub fn inspect_resolved_plugin_static(record: &ResolvedPluginRecord) -> PluginStaticInspection {
let runtime = match &record.manifest.runtime {
Some(runtime)
if runtime.kind == "wasm" && runtime.abi.as_deref() == Some("yoi-plugin-wasm-1") =>
if runtime.kind == PLUGIN_RUNTIME_WASM_KIND
&& runtime.abi.as_deref() == Some(PLUGIN_RUNTIME_WASM_ABI)
&& runtime.entry.is_some() =>
{
PluginRuntimeEligibility {
eligible: true,
status: "wasm/yoi-plugin-wasm-1".to_string(),
status: format!("{PLUGIN_RUNTIME_WASM_KIND}/{PLUGIN_RUNTIME_WASM_ABI}"),
diagnostic: None,
}
}
Some(runtime) if runtime.kind == "wasm" => {
Some(runtime) if runtime.kind == PLUGIN_RUNTIME_WASM_KIND => {
let status = runtime
.abi
.as_deref()
.map(|abi| format!("wasm/{abi}"))
.unwrap_or_else(|| "wasm/<missing-abi>".to_string());
.map(|abi| format!("{PLUGIN_RUNTIME_WASM_KIND}/{abi}"))
.unwrap_or_else(|| format!("{PLUGIN_RUNTIME_WASM_KIND}/<missing-abi>"));
PluginRuntimeEligibility {
eligible: false,
status,
diagnostic: Some("unsupported or missing plugin runtime ABI".to_string()),
}
}
Some(runtime)
if runtime.kind == PLUGIN_RUNTIME_COMPONENT_KIND
&& runtime.world.as_deref() == Some(PLUGIN_COMPONENT_TOOL_WORLD)
&& runtime.component.is_some() =>
{
PluginRuntimeEligibility {
eligible: true,
status: format!("{PLUGIN_RUNTIME_COMPONENT_KIND}/{PLUGIN_COMPONENT_TOOL_WORLD}"),
diagnostic: None,
}
}
Some(runtime) if runtime.kind == PLUGIN_RUNTIME_COMPONENT_KIND => {
let status = runtime
.world
.as_deref()
.map(|world| format!("{PLUGIN_RUNTIME_COMPONENT_KIND}/{world}"))
.unwrap_or_else(|| format!("{PLUGIN_RUNTIME_COMPONENT_KIND}/<missing-world>"));
PluginRuntimeEligibility {
eligible: false,
status,
diagnostic: Some("unsupported or missing plugin component world".to_string()),
}
}
Some(runtime) => PluginRuntimeEligibility {
eligible: false,
status: runtime.kind.clone(),
@ -1484,6 +1511,17 @@ const PLUGIN_FS_MAX_READ_BYTES: usize = 64 * 1024;
const PLUGIN_FS_MAX_WRITE_BYTES: usize = 64 * 1024;
const PLUGIN_FS_MAX_LIST_ENTRIES: usize = 256;
fn wasm_component_store_limits() -> wasmtime::StoreLimits {
wasmtime::StoreLimitsBuilder::new()
.memory_size(PLUGIN_WASM_MEMORY_BYTES)
.table_elements(PLUGIN_WASM_TABLE_ELEMENTS)
.instances(1)
.tables(1)
.memories(1)
.trap_on_grow_failure(true)
.build()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PluginFsRuntimeOperation {
Read,
@ -1708,26 +1746,25 @@ impl Tool for PluginWasmTool {
let plugin_ref = self.origin.plugin_ref.clone();
let digest = self.origin.digest.clone();
let input = input_json.as_bytes().to_vec();
let execution =
tokio::task::spawn_blocking(move || run_plugin_wasm_tool(record, name, input));
let execution = tokio::task::spawn_blocking(move || run_plugin_tool(record, name, input));
match tokio::time::timeout(PLUGIN_WASM_TIMEOUT, execution).await {
Ok(Ok(Ok(output))) => Ok(output),
Ok(Ok(Err(error))) => Err(ToolError::ExecutionFailed(format!(
"plugin WASM tool `{}` from `{}` (digest {}) failed closed: {}",
"plugin tool `{}` from `{}` (digest {}) failed closed: {}",
self.name,
plugin_ref,
digest,
error.bounded_message()
))),
Ok(Err(error)) => Err(ToolError::ExecutionFailed(format!(
"plugin WASM tool `{}` from `{}` (digest {}) cancelled/failed to join: {}",
"plugin tool `{}` from `{}` (digest {}) cancelled/failed to join: {}",
self.name,
plugin_ref,
digest,
bounded_message(error.to_string())
))),
Err(_) => Err(ToolError::ExecutionFailed(format!(
"plugin WASM tool `{}` from `{}` (digest {}) timed out after {:?}",
"plugin tool `{}` from `{}` (digest {}) timed out after {:?}",
self.name, plugin_ref, digest, PLUGIN_WASM_TIMEOUT
))),
}
@ -1767,6 +1804,28 @@ struct PluginWasmHostState {
store_limits: wasmi::StoreLimits,
}
fn run_plugin_tool(
record: ResolvedPluginRecord,
tool_name: String,
input: Vec<u8>,
) -> Result<ToolOutput, PluginWasmError> {
match record
.manifest
.runtime
.as_ref()
.map(|runtime| runtime.kind.as_str())
{
Some(PLUGIN_RUNTIME_WASM_KIND) => run_plugin_wasm_tool(record, tool_name, input),
Some(PLUGIN_RUNTIME_COMPONENT_KIND) => run_plugin_component_tool(record, tool_name, input),
Some(other) => Err(PluginWasmError::Module(format!(
"unsupported plugin runtime kind `{other}`"
))),
None => Err(PluginWasmError::Package(
"plugin runtime is not declared".to_string(),
)),
}
}
fn run_plugin_wasm_tool(
record: ResolvedPluginRecord,
tool_name: String,
@ -1864,6 +1923,225 @@ fn run_plugin_wasm_tool_with_https_client(
decode_plugin_wasm_output(&store.data().output)
}
#[derive(Clone)]
struct PluginComponentHostState {
record: ResolvedPluginRecord,
https_client: Arc<dyn PluginHttpsClient>,
store_limits: wasmtime::StoreLimits,
}
fn run_plugin_component_tool(
record: ResolvedPluginRecord,
tool_name: String,
input: Vec<u8>,
) -> Result<ToolOutput, PluginWasmError> {
run_plugin_component_tool_with_https_client(
record,
tool_name,
input,
Arc::new(ReqwestPluginHttpsClient),
)
}
fn run_plugin_component_tool_with_https_client(
record: ResolvedPluginRecord,
tool_name: String,
input: Vec<u8>,
https_client: Arc<dyn PluginHttpsClient>,
) -> Result<ToolOutput, PluginWasmError> {
let tool = record
.manifest
.tools
.iter()
.find(|tool| tool.name == tool_name)
.ok_or_else(|| {
PluginWasmError::Module("requested tool is not declared by plugin manifest".to_string())
})?;
authorize_plugin_tool(&record, tool).map_err(|error| {
PluginWasmError::Module(format!(
"plugin permission denied: {}",
error.bounded_message()
))
})?;
let limits = PluginDiscoveryLimits::default();
let component_bytes = read_resolved_plugin_runtime_component(&record, &limits)
.map_err(|diagnostic| PluginWasmError::Package(diagnostic.message))?;
if component_bytes.len() > limits.max_file_size_bytes as usize {
return Err(PluginWasmError::Package(format!(
"WASM component runtime artifact exceeds {} bytes",
limits.max_file_size_bytes
)));
}
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.consume_fuel(true);
config.max_wasm_stack(8 * 1024 * 1024);
let engine = wasmtime::Engine::new(&config)
.map_err(|error| PluginWasmError::Module(error.to_string()))?;
let component =
wasmtime::component::Component::new(&engine, &component_bytes).map_err(|error| {
PluginWasmError::Module(format!("component is incompatible: {error:?}"))
})?;
validate_component_imports(&record, &engine, &component)?;
let mut linker = wasmtime::component::Linker::<PluginComponentHostState>::new(&engine);
define_plugin_component_host_imports(&mut linker)?;
let mut store = wasmtime::Store::new(
&engine,
PluginComponentHostState {
record: record.clone(),
https_client,
store_limits: wasm_component_store_limits(),
},
);
store.limiter(|state| &mut state.store_limits);
store
.set_fuel(PLUGIN_WASM_FUEL)
.map_err(|error| PluginWasmError::Execution(error.to_string()))?;
let instance = linker
.instantiate(&mut store, &component)
.map_err(|error| PluginWasmError::Execution(error.to_string()))?;
let call = instance
.get_typed_func::<(&str, &str), (String,)>(&mut store, "call")
.map_err(|error| {
PluginWasmError::Module(format!(
"component does not export expected `{}` call function: {error}",
PLUGIN_COMPONENT_TOOL_WORLD
))
})?;
let input_json = std::str::from_utf8(&input).map_err(|error| {
PluginWasmError::Output(format!("plugin component input is not UTF-8: {error}"))
})?;
// Wasmtime lifts the returned WIT `string` into a host `String` before the
// ordinary ToolOutput JSON cap can be applied. Keep the component store on
// the same memory/table/instance limits as the raw WASM runtime so an
// untrusted component can only force host string allocation from bounded
// component memory; oversized memories/tables/instances fail closed during
// instantiation/growth before this lift succeeds.
let (output,) = call
.call(&mut store, (&tool_name, input_json))
.map_err(|error| PluginWasmError::Execution(error.to_string()))?;
decode_plugin_wasm_output(output.as_bytes())
}
fn validate_component_imports(
record: &ResolvedPluginRecord,
engine: &wasmtime::Engine,
component: &wasmtime::component::Component,
) -> Result<(), PluginWasmError> {
for (name, _) in component.component_type().imports(engine) {
match name {
"yoi:host/https@1.0.0" => {
authorize_plugin_host_api(record, PluginHostApi::Https).map_err(|error| {
PluginWasmError::Module(format!(
"plugin host API dispatch denied: {}",
error.bounded_message()
))
})?;
}
"yoi:host/fs@1.0.0" => {
authorize_plugin_host_api(record, PluginHostApi::Fs).map_err(|error| {
PluginWasmError::Module(format!(
"plugin host API dispatch denied: {}",
error.bounded_message()
))
})?;
}
other => {
return Err(PluginWasmError::Module(format!(
"unsupported component import `{other}`; no WASI filesystem, ambient network, environment, or other imports are available"
)));
}
}
}
Ok(())
}
fn define_plugin_component_host_imports(
linker: &mut wasmtime::component::Linker<PluginComponentHostState>,
) -> Result<(), PluginWasmError> {
linker
.root()
.instance("yoi:host/https@1.0.0")
.map_err(|error| PluginWasmError::Module(error.to_string()))?
.func_wrap(
"request",
|store: wasmtime::StoreContextMut<'_, PluginComponentHostState>,
(request,): (String,)|
-> wasmtime::Result<(String,)> {
authorize_plugin_host_api(&store.data().record, PluginHostApi::Https)
.map_err(|error| wasmtime::Error::msg(error.bounded_message()))?;
let response = execute_plugin_https_request(
&store.data().record,
store.data().https_client.as_ref(),
request.as_bytes(),
)
.map_err(|error| wasmtime::Error::msg(error.0))?;
Ok((String::from_utf8_lossy(&response).into_owned(),))
},
)
.map_err(|error| PluginWasmError::Module(error.to_string()))?;
let mut root = linker.root();
let mut fs = root
.instance("yoi:host/fs@1.0.0")
.map_err(|error| PluginWasmError::Module(error.to_string()))?;
fs.func_wrap(
"read",
|store: wasmtime::StoreContextMut<'_, PluginComponentHostState>,
(request,): (String,)|
-> wasmtime::Result<(String,)> {
authorize_plugin_host_api(&store.data().record, PluginHostApi::Fs)
.map_err(|error| wasmtime::Error::msg(error.bounded_message()))?;
execute_plugin_fs_request(
&store.data().record,
PluginFsRuntimeOperation::Read,
request.as_bytes(),
)
.map(|bytes| (String::from_utf8_lossy(&bytes).into_owned(),))
.map_err(|error| wasmtime::Error::msg(error.message))
},
)
.map_err(|error| PluginWasmError::Module(error.to_string()))?;
fs.func_wrap(
"list",
|store: wasmtime::StoreContextMut<'_, PluginComponentHostState>,
(request,): (String,)|
-> wasmtime::Result<(String,)> {
authorize_plugin_host_api(&store.data().record, PluginHostApi::Fs)
.map_err(|error| wasmtime::Error::msg(error.bounded_message()))?;
execute_plugin_fs_request(
&store.data().record,
PluginFsRuntimeOperation::List,
request.as_bytes(),
)
.map(|bytes| (String::from_utf8_lossy(&bytes).into_owned(),))
.map_err(|error| wasmtime::Error::msg(error.message))
},
)
.map_err(|error| PluginWasmError::Module(error.to_string()))?;
fs.func_wrap(
"write",
|store: wasmtime::StoreContextMut<'_, PluginComponentHostState>,
(request,): (String,)|
-> wasmtime::Result<(String,)> {
authorize_plugin_host_api(&store.data().record, PluginHostApi::Fs)
.map_err(|error| wasmtime::Error::msg(error.bounded_message()))?;
execute_plugin_fs_request(
&store.data().record,
PluginFsRuntimeOperation::Write,
request.as_bytes(),
)
.map(|bytes| (String::from_utf8_lossy(&bytes).into_owned(),))
.map_err(|error| wasmtime::Error::msg(error.message))
},
)
.map_err(|error| PluginWasmError::Module(error.to_string()))?;
Ok(())
}
fn validate_wasm_imports(
record: &ResolvedPluginRecord,
module: &wasmi::Module,
@ -3679,9 +3957,11 @@ mod tests {
fn record_with_missing_package_runtime() -> ResolvedPluginRecord {
let mut record = record(vec![tool("PluginSearch")]);
record.manifest.runtime = Some(PluginRuntimeManifest {
kind: "wasm".into(),
entry: "plugin.wasm".into(),
abi: Some("yoi-plugin-wasm-1".into()),
kind: PLUGIN_RUNTIME_WASM_KIND.into(),
entry: Some("plugin.wasm".into()),
abi: Some(PLUGIN_RUNTIME_WASM_ABI.into()),
component: None,
world: None,
});
record
}
@ -3736,6 +4016,347 @@ mod tests {
(dir, record)
}
fn write_component_plugin_package(path: &Path, component: &[u8], world: &str) {
let manifest = format!(
r#"schema_version = 1
id = "example"
name = "Example"
version = "1.0.0"
description = "Example component plugin"
surfaces = ["tool"]
[runtime]
kind = "wasm-component"
component = "plugin.component.wasm"
world = "{}"
[[permissions]]
kind = "surface"
surface = "tool"
[[permissions]]
kind = "tool"
name = "PluginEcho"
[[tools]]
name = "PluginEcho"
description = "Echo plugin tool"
input_schema = {{ type = "object", additionalProperties = true }}
"#,
world
);
write_stored_zip(
path,
&[
("plugin.toml", manifest.as_bytes()),
("plugin.component.wasm", component),
],
);
}
fn resolved_record_with_component(component: Vec<u8>) -> (TempDir, ResolvedPluginRecord) {
let dir = TempDir::new().unwrap();
let package_dir = dir.path().join(".yoi/plugins");
fs::create_dir_all(&package_dir).unwrap();
let package_path = package_dir.join("component.yoi-plugin");
write_component_plugin_package(&package_path, &component, PLUGIN_COMPONENT_TOOL_WORLD);
let config = PluginConfig {
enabled: vec![PluginEnablementConfig {
id: "project:example".parse().unwrap(),
surfaces: vec![PluginSurface::Tool],
..PluginEnablementConfig::default()
}],
resolved: Vec::new(),
diagnostics: Vec::new(),
};
let options = PluginDiscoveryOptions::new(dir.path());
let resolved = resolve_plugin_config_for_startup(&config, &options);
assert!(
resolved.diagnostics.is_empty(),
"{:#?}",
resolved.diagnostics
);
assert_eq!(resolved.resolved.len(), 1);
let mut record = resolved.resolved[0].clone();
record.grants = PluginGrantConfig {
id: Some(record.identity.to_string()),
version: Some(PluginExactVersion(record.version.clone())),
digest: Some(record.digest.clone()),
permissions: tool_permissions(&record.manifest.tools),
https: Vec::new(),
fs: Vec::new(),
};
(dir, record)
}
fn component_tool_that_returns(output: &[u8]) -> Vec<u8> {
component_tool_with_memory_pages(output, 1)
}
fn component_tool_with_memory_pages(output: &[u8], memory_pages: usize) -> Vec<u8> {
wat::parse_str(format!(
r#"(component
(core module $m
(memory (export "memory") {})
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
(if (result i32) (i32.eqz (local.get 0))
(then (i32.const 8192))
(else (local.get 0))))
(data (i32.const 1024) "{}")
(func (export "call") (param i32 i32 i32 i32) (result i32)
(i32.store (i32.const 2048) (i32.const 1024))
(i32.store (i32.const 2052) (i32.const {}))
(i32.const 2048))
)
(core instance $i (instantiate $m))
(alias core export $i "memory" (core memory $mem))
(alias core export $i "realloc" (core func $realloc))
(alias core export $i "call" (core func $call_core))
(type $call_ty (func (param "tool-name" string) (param "input-json" string) (result string)))
(func $call (type $call_ty) (canon lift (core func $call_core) (memory $mem) (realloc $realloc) string-encoding=utf8))
(export "call" (func $call))
)"#,
memory_pages,
wat_bytes(output),
output.len()
))
.expect("valid component wat")
}
fn component_tool_with_table_elements(output: &[u8], table_elements: usize) -> Vec<u8> {
wat::parse_str(format!(
r#"(component
(core module $m
(memory (export "memory") 1)
(table {} funcref)
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
(if (result i32) (i32.eqz (local.get 0))
(then (i32.const 8192))
(else (local.get 0))))
(data (i32.const 1024) "{}")
(func (export "call") (param i32 i32 i32 i32) (result i32)
(i32.store (i32.const 2048) (i32.const 1024))
(i32.store (i32.const 2052) (i32.const {}))
(i32.const 2048))
)
(core instance $i (instantiate $m))
(alias core export $i "memory" (core memory $mem))
(alias core export $i "realloc" (core func $realloc))
(alias core export $i "call" (core func $call_core))
(type $call_ty (func (param "tool-name" string) (param "input-json" string) (result string)))
(func $call (type $call_ty) (canon lift (core func $call_core) (memory $mem) (realloc $realloc) string-encoding=utf8))
(export "call" (func $call))
)"#,
table_elements,
wat_bytes(output),
output.len()
))
.expect("valid component wat")
}
fn component_tool_importing_https(output: &[u8]) -> Vec<u8> {
wat::parse_str(format!(
r#"(component
(import "yoi:host/https@1.0.0" (instance $https (export "request" (func $request (param "request-json" string) (result string)))))
(core module $m
(memory (export "memory") 1)
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
(if (result i32) (i32.eqz (local.get 0))
(then (i32.const 8192))
(else (local.get 0))))
(data (i32.const 1024) "{}")
(func (export "call") (param i32 i32 i32 i32) (result i32)
(i32.store (i32.const 2048) (i32.const 1024))
(i32.store (i32.const 2052) (i32.const {}))
(i32.const 2048))
)
(core instance $i (instantiate $m))
(alias core export $i "memory" (core memory $mem))
(alias core export $i "realloc" (core func $realloc))
(alias core export $i "call" (core func $call_core))
(type $call_ty (func (param "tool-name" string) (param "input-json" string) (result string)))
(func $call (type $call_ty) (canon lift (core func $call_core) (memory $mem) (realloc $realloc) string-encoding=utf8))
(export "call" (func $call))
)"#,
wat_bytes(output),
output.len()
))
.expect("valid component wat")
}
fn component_without_call_export() -> Vec<u8> {
wat::parse_str(r#"(component (core module $m) (core instance $i (instantiate $m)))"#)
.unwrap()
}
fn raw_module_bytes() -> Vec<u8> {
wat::parse_str(r#"(module (func (export "call")))"#).unwrap()
}
#[test]
fn component_tool_executes_through_ordinary_tool_result_path() {
let (_dir, record) = resolved_record_with_component(component_tool_that_returns(
br#"{"summary":"component ok","content":"ordinary tool result path"}"#,
));
let output = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect("component tool output");
assert_eq!(output.summary, "component ok");
assert_eq!(output.content.as_deref(), Some("ordinary tool result path"));
}
#[test]
fn component_memory_limit_fails_closed_before_string_lift() {
let oversized_memory_pages = (PLUGIN_WASM_MEMORY_BYTES / 65_536) + 1;
let (_dir, record) = resolved_record_with_component(component_tool_with_memory_pages(
br#"{"summary":"should not lift"}"#,
oversized_memory_pages,
));
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect_err("component memory limit is enforced");
assert!(format!("{error:?}").contains("growing memory"), "{error:?}");
}
#[test]
fn component_table_limit_fails_closed() {
let (_dir, record) = resolved_record_with_component(component_tool_with_table_elements(
br#"{"summary":"should not run"}"#,
PLUGIN_WASM_TABLE_ELEMENTS + 1,
));
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect_err("component table limit is enforced");
assert!(format!("{error:?}").contains("growing table"), "{error:?}");
}
#[test]
fn component_output_cap_still_fails_closed_after_bounded_lift() {
let output = format!(
r#"{{"summary":"too big","content":"{}"}}"#,
"x".repeat(PLUGIN_WASM_MAX_OUTPUT_BYTES)
);
let (_dir, record) =
resolved_record_with_component(component_tool_with_memory_pages(output.as_bytes(), 2));
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect_err("component output cap is enforced");
assert!(format!("{error:?}").contains("output exceeds"), "{error:?}");
}
#[test]
fn component_tool_denies_host_import_without_matching_grant() {
let (_dir, record) = resolved_record_with_component(component_tool_importing_https(
br#"{"summary":"component ok"}"#,
));
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect_err("host import without grant is denied");
assert!(
format!("{error:?}").contains("plugin host API dispatch denied"),
"{error:?}"
);
}
#[test]
fn component_tool_missing_export_fails_closed() {
let (_dir, record) = resolved_record_with_component(component_without_call_export());
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect_err("missing export fails closed");
assert!(
format!("{error:?}").contains("does not export expected"),
"{error:?}"
);
}
#[test]
fn core_wasm_is_not_silently_reinterpreted_as_component() {
let (_dir, record) = resolved_record_with_component(raw_module_bytes());
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
.expect_err("core module is incompatible with component runtime");
assert!(
format!("{error:?}").contains("component is incompatible"),
"{error:?}"
);
}
#[test]
fn component_wrong_world_fails_closed_during_discovery() {
let dir = TempDir::new().unwrap();
let package_dir = dir.path().join(".yoi/plugins");
fs::create_dir_all(&package_dir).unwrap();
let package_path = package_dir.join("component.yoi-plugin");
write_component_plugin_package(
&package_path,
&component_tool_that_returns(br#"{"summary":"component ok"}"#),
"example:other/world@1.0.0",
);
let config = PluginConfig {
enabled: vec![PluginEnablementConfig {
id: "project:example".parse().unwrap(),
surfaces: vec![PluginSurface::Tool],
..PluginEnablementConfig::default()
}],
resolved: Vec::new(),
diagnostics: Vec::new(),
};
let options = PluginDiscoveryOptions::new(dir.path());
let resolved = resolve_plugin_config_for_startup(&config, &options);
assert!(resolved.resolved.is_empty());
assert!(
resolved.diagnostics.iter().any(|diagnostic| diagnostic
.message
.contains("component world is unsupported")),
"{:#?}",
resolved.diagnostics
);
}
#[test]
fn component_tool_registration_uses_existing_tool_registry_path() {
let (_dir, record) = resolved_record_with_component(component_tool_that_returns(
br#"{"summary":"component ok"}"#,
));
let (report, pending) = install_plugin_record(record);
assert_eq!(skipped_count(&report), 0, "{report:#?}");
assert_eq!(pending.len(), 1);
let (meta, _) = pending[0]();
assert_eq!(meta.name, "PluginEcho");
}
#[test]
fn component_static_inspection_reports_component_runtime_without_execution() {
let mut record = record(vec![tool("Echo")]);
record.package_path = std::path::PathBuf::from("/no/such/component.wasm");
record.manifest.runtime = Some(PluginRuntimeManifest {
kind: PLUGIN_RUNTIME_COMPONENT_KIND.to_string(),
entry: None,
abi: None,
component: Some("plugin.component.wasm".to_string()),
world: Some(PLUGIN_COMPONENT_TOOL_WORLD.to_string()),
});
let inspection = inspect_resolved_plugin_static(&record);
assert!(inspection.runtime.eligible);
assert_eq!(
inspection.runtime.status,
format!("{PLUGIN_RUNTIME_COMPONENT_KIND}/{PLUGIN_COMPONENT_TOOL_WORLD}")
);
assert!(inspection.runtime.diagnostic.is_none());
}
fn write_plugin_package(path: &Path, wasm: &[u8]) {
let manifest = br#"schema_version = 1
id = "example"
@ -3884,9 +4505,11 @@ input_schema = { type = "object", additionalProperties = true }
let mut record = record(vec![tool("Echo")]);
record.package_path = std::path::PathBuf::from("/no/such/plugin.wasm");
record.manifest.runtime = Some(PluginRuntimeManifest {
kind: "wasm".to_string(),
entry: "plugin.wasm".to_string(),
abi: Some("yoi-plugin-wasm-1".to_string()),
kind: PLUGIN_RUNTIME_WASM_KIND.to_string(),
entry: Some("plugin.wasm".to_string()),
abi: Some(PLUGIN_RUNTIME_WASM_ABI.to_string()),
component: None,
world: None,
});
let inspection = inspect_resolved_plugin_static(&record);
@ -3901,9 +4524,11 @@ input_schema = { type = "object", additionalProperties = true }
fn static_inspection_reports_missing_tool_grant() {
let mut record = record(vec![tool("Echo")]);
record.manifest.runtime = Some(PluginRuntimeManifest {
kind: "wasm".to_string(),
entry: "plugin.wasm".to_string(),
abi: Some("yoi-plugin-wasm-1".to_string()),
kind: PLUGIN_RUNTIME_WASM_KIND.to_string(),
entry: Some("plugin.wasm".to_string()),
abi: Some(PLUGIN_RUNTIME_WASM_ABI.to_string()),
component: None,
world: None,
});
record.grants.permissions = vec![PluginPermission::surface(PluginSurface::Tool)];
@ -3926,9 +4551,11 @@ input_schema = { type = "object", additionalProperties = true }
bad_schema.input_schema = json!({"type":"string"});
let mut record = record(vec![bad_schema]);
record.manifest.runtime = Some(PluginRuntimeManifest {
kind: "wasm".to_string(),
entry: "plugin.wasm".to_string(),
abi: Some("yoi-plugin-wasm-1".to_string()),
kind: PLUGIN_RUNTIME_WASM_KIND.to_string(),
entry: Some("plugin.wasm".to_string()),
abi: Some(PLUGIN_RUNTIME_WASM_ABI.to_string()),
component: None,
world: None,
});
let inspection = inspect_resolved_plugin_static(&record);
@ -3953,9 +4580,11 @@ input_schema = { type = "object", additionalProperties = true }
second_duplicate.input_schema = json!({"type":"object"});
let mut record = record(vec![invalid, first_duplicate, second_duplicate]);
record.manifest.runtime = Some(PluginRuntimeManifest {
kind: "wasm".to_string(),
entry: "plugin.wasm".to_string(),
abi: Some("yoi-plugin-wasm-1".to_string()),
kind: PLUGIN_RUNTIME_WASM_KIND.to_string(),
entry: Some("plugin.wasm".to_string()),
abi: Some(PLUGIN_RUNTIME_WASM_ABI.to_string()),
component: None,
world: None,
});
let inspection = inspect_resolved_plugin_static(&record);

View File

@ -121,3 +121,57 @@ For example, `https` should be modeled as typed request/response data with expli
- It does not replace Plugin grants with WIT imports.
- It does not introduce Service, Ingress, WebSocket, or inbound HTTP by itself.
- It does not merge Plugin and MCP. MCP remains a separate untrusted tool/resource/prompt bridge with its own policy.
## Implemented runtime boundary
Plugin Tool packages now select the runtime explicitly in `plugin.toml`:
```toml
[runtime]
kind = "wasm-component"
component = "plugin.component.wasm"
world = "yoi:plugin/tool@1.0.0"
```
The legacy core-Wasm ABI remains explicit and is not reinterpreted as a
component:
```toml
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
```
The component runtime uses `wasmtime::component` and expects the exported world
`yoi:plugin/tool@1.0.0` with a `call(tool-name: string, input-json: string) ->
string` export. The returned string is the same ToolOutput JSON used by the raw
runtime, so registration and execution still flow through the existing
ToolRegistry and Worker Tool-result history path.
Host imports are stable names under `yoi:host/*@1.0.0`; the repository WIT files
live in `resources/plugin/wit/`. Importing `yoi:host/https@1.0.0` or
`yoi:host/fs@1.0.0` is not authority. The runtime checks package grants before
component instantiation and checks again on every host call. No WASI filesystem,
network, environment, or other ambient imports are linked.
Static discovery and `yoi plugin list/show` only parse package manifests and
reported runtime metadata. They do not instantiate or execute the component.
Wrong `world`, missing artifact metadata, missing `call` export, unsupported
imports, or core-Wasm bytes in a component package all fail closed with bounded
Plugin diagnostics or ordinary Tool errors.
See `docs/examples/plugin-component-tool/lib.rs` for a minimal
`wit-bindgen`/SDK-style authoring sketch. Package authors should generate
bindings from `resources/plugin/wit`, build a component artifact, and set the
component runtime metadata above.
### v1 request/response shape
The v1 component world intentionally keeps Tool input, Tool output, and host API
payloads as JSON strings. This is a migration bridge that preserves the existing
ToolOutput schema, Tool history behavior, grant checks, and raw-Wasm host API
semantics while moving package authors onto WIT/canonical ABI bindings.
Structured WIT records for Tool requests/responses/errors and host HTTPS/FS
payloads are deferred to a follow-up API-design step rather than accidentally
omitted.

View File

@ -204,3 +204,29 @@ Good follow-up Tickets are intentionally separable:
6. WASM package ABI, initialization limits, host-function grants, and Tool/Hook contribution plumbing.
7. Optional lock-file or pin update workflow for reproducible fresh startup.
8. Future MCP/plugin bridge, only if explicitly approved as a separate design and implementation effort.
### Component Model Tool runtime
Tool packages may use WebAssembly Component Model runtime metadata:
```toml
[runtime]
kind = "wasm-component"
component = "plugin.component.wasm"
world = "yoi:plugin/tool@1.0.0"
```
This is separate from the legacy raw core-Wasm runtime:
```toml
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
```
Component packages must not use `entry`/`abi`; raw packages must not use
`component`/`world`. Discovery reports the selected runtime kind/world without
executing the artifact. Component execution still requires explicit package
enablement, exact source/version/digest grants, and matching Tool/host API
permissions.

View File

@ -0,0 +1,23 @@
//! Minimal Component Model Tool plugin authoring sketch.
//!
//! Build this as a `wasm32-unknown-unknown` cdylib with `wit-bindgen`-generated
//! exports and package the adapted component as `plugin.component.wasm`.
wit_bindgen::generate!({
world: "tool",
path: "../../../resources/plugin/wit",
});
struct Plugin;
impl Guest for Plugin {
fn call(tool_name: String, input_json: String) -> String {
// Ordinary ToolOutput JSON. The runtime routes this through the normal
// Worker/Tool result path; no context is injected by the component.
format!(
r#"{{"summary":"component tool {tool_name}","content":"input was {input_json}"}}"#
)
}
}
export!(Plugin);

View File

@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-xqax43t9IevkNG2lZvfRP562ORKb3aHxUNsQwS1FK/k=";
cargoHash = "sha256-i4U7wXPoWIHA4EAJZva2HQXNN8P5+RhGVGNBAOZVGk0=";
depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint,

View File

@ -0,0 +1,15 @@
package yoi:host@1.0.0;
/// Grant-bound HTTPS host API. Importing this interface does not grant
/// authority; package grants are checked before registration/execution and on
/// every host call.
interface https {
request: func(request-json: string) -> string;
}
/// Grant-bound filesystem host API. No ambient WASI filesystem is exposed.
interface fs {
read: func(request-json: string) -> string;
list: func(request-json: string) -> string;
write: func(request-json: string) -> string;
}

View File

@ -0,0 +1,11 @@
package yoi:plugin@1.0.0;
world tool {
import yoi:host/https@1.0.0;
import yoi:host/fs@1.0.0;
/// Execute a manifest-declared Tool. `input-json` is the normal Tool input
/// JSON and the returned string is the same ToolOutput JSON accepted by the
/// legacy raw-Wasm ABI.
export call: func(tool-name: string, input-json: string) -> string;
}