From c04906e7a8972ee107b9263874bc8e7151eec5f9 Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 25 May 2026 18:13:53 -0400 Subject: [PATCH] feat(homecore-plugins/p1): ADR-128 plugin runtime scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `v2/crates/homecore-plugins` (0.1.0-alpha.0) — the P1 scaffold for the HOMECORE-PLUGINS WASM integration system (ADR-128): - `manifest.rs`: `PluginManifest` — superset of HA manifest.json; serde round-trip + required-field validation (`domain`/`name`/`version`). - `error.rs`: `PluginError` typed enum (InvalidManifest, AlreadyLoaded, NotFound, RuntimeError, SetupFailed, UnloadFailed, Io). - `plugin.rs`: `HomeCorePlugin` async trait + `PluginId` newtype. - `runtime.rs`: `PluginRuntime` trait + `InProcessRuntime` (native Rust, first-party plugins). `WasmtimeRuntime` stub gated on `--features wasmtime` (default-off; 30 MB dep deferred to P2). - `registry.rs`: `PluginRegistry` — load/unload/list/contains via RwLock. - 10 unit tests, 0 failed. Wasmtime vs wasm3 runtime selection is still open (ADR-128 §8 Q2); this scaffold makes the choice swappable via the `PluginRuntime` trait. The `wasmtime` and `wasm3` features are default-off; P2 resolves the choice and wires host ABI (`hc_state_get`/`hc_state_set`/etc.) to ADR-127. Co-Authored-By: claude-flow --- v2/Cargo.lock | 941 ++++++++++++++++++++- v2/Cargo.toml | 2 + v2/crates/homecore-plugins/Cargo.toml | 57 ++ v2/crates/homecore-plugins/src/error.rs | 35 + v2/crates/homecore-plugins/src/lib.rs | 51 ++ v2/crates/homecore-plugins/src/manifest.rs | 144 ++++ v2/crates/homecore-plugins/src/plugin.rs | 59 ++ v2/crates/homecore-plugins/src/registry.rs | 102 +++ v2/crates/homecore-plugins/src/runtime.rs | 119 +++ v2/crates/homecore-plugins/src/tests.rs | 233 +++++ 10 files changed, 1714 insertions(+), 29 deletions(-) create mode 100644 v2/crates/homecore-plugins/Cargo.toml create mode 100644 v2/crates/homecore-plugins/src/error.rs create mode 100644 v2/crates/homecore-plugins/src/lib.rs create mode 100644 v2/crates/homecore-plugins/src/manifest.rs create mode 100644 v2/crates/homecore-plugins/src/plugin.rs create mode 100644 v2/crates/homecore-plugins/src/registry.rs create mode 100644 v2/crates/homecore-plugins/src/runtime.rs create mode 100644 v2/crates/homecore-plugins/src/tests.rs diff --git a/v2/Cargo.lock b/v2/Cargo.lock index 264d8429..bbbf885c 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -19,6 +28,18 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -189,6 +210,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object 0.37.3", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -816,7 +846,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 1.3.0", ] [[package]] @@ -949,6 +979,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[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 = "cog-ha-matter" version = "0.3.0" @@ -1180,6 +1219,15 @@ dependencies = [ "libc", ] +[[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 = "cpu-time" version = "1.0.0" @@ -1208,6 +1256,128 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-bforest" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69792bd40d21be8059f7c709f44200ded3bbd073df7eb3fa3c282b387c7ffa5b" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da1eb6f7d8cdfa92f05acfae63c9a1d7a337e49ce7a2d0769c7fa03a2613a5" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709f5567a2bff9f06edf911a7cb5ebb091e4c81701714dc6ab574d08b4a69a0d" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d39a6b194c069fd091ca1f17b9d86ff1a4627ccad8806095828f61989a691f" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f81aefad1f80ed4132ae33f40b92779eeb57edeb1e28bb24424a4098c963a2" + +[[package]] +name = "cranelift-control" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6adbaac785ad4683c4f199686f9e15c1471f52ae2f4c013a3be039b4719db754" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b85ed43567e13782cd1b25baf42a8167ee57169a60dfd3d7307c6ca3839da0" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8349f71373bb69c6f73992c6c1606236a66c8134e7a60e04e03fbd64b1aa7dcf" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a6b958ce05e0c237c8b25508012b6c644e8c37348213a8c786ba29e28cfdb" + +[[package]] +name = "cranelift-native" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4acaf6894ee323ff4e9ce786bec09f0ebbe49941e8012f1c1052f1d965034" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.112.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b878860895cca97454ef8d8b12bfda9d0889dd49efee175dba78d54ff8363ec2" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools 0.12.1", + "log", + "smallvec", + "wasmparser 0.217.1", + "wasmtime-types", +] + [[package]] name = "crc" version = "3.4.0" @@ -1404,6 +1574,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "cudarc" version = "0.17.8" @@ -1507,6 +1683,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "der" version = "0.7.10" @@ -1569,6 +1754,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1611,6 +1806,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -1779,6 +1985,18 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[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 = "encode_unicode" version = "1.0.0" @@ -1856,6 +2074,12 @@ 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 = "fastbloom" version = "0.14.1" @@ -2158,6 +2382,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags 2.11.0", + "debugid", + "fxhash", + "serde", + "serde_json", +] + [[package]] name = "gdk" version = "0.18.2" @@ -2735,6 +2972,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +dependencies = [ + "fallible-iterator", + "indexmap 2.13.0", + "stable_deref_trait", +] + [[package]] name = "gio" version = "0.18.4" @@ -2797,7 +3045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2", "quote", @@ -2955,6 +3203,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] [[package]] name = "hashbrown" @@ -3089,6 +3341,57 @@ dependencies = [ "serde", ] +[[package]] +name = "homecore" +version = "0.1.0-alpha.0" +dependencies = [ + "async-trait", + "chrono", + "dashmap", + "futures", + "once_cell", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "uuid", +] + +[[package]] +name = "homecore-api" +version = "0.1.0-alpha.0" +dependencies = [ + "axum", + "chrono", + "dashmap", + "homecore", + "http-body-util", + "hyper 1.8.1", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tower 0.5.3", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "homecore-plugins" +version = "0.1.0-alpha.0" +dependencies = [ + "async-trait", + "homecore", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "wasm3", + "wasmtime", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -3565,12 +3868,41 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + [[package]] name = "javascriptcore-rs" version = "1.1.2" @@ -3745,6 +4077,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -3849,6 +4187,12 @@ dependencies = [ "pkg-config", ] +[[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" @@ -3987,6 +4331,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" @@ -4713,6 +5066,27 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -5320,6 +5694,18 @@ dependencies = [ "portable-atomic", ] +[[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.4" @@ -5411,11 +5797,10 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime 0.6.3", "toml_edit 0.20.2", ] @@ -5508,6 +5893,16 @@ dependencies = [ "unarray", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "ptr_meta" version = "0.3.1" @@ -5962,6 +6357,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "regalloc2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12908dbeb234370af84d0579b9f68258a0f67e201412dd9a2814e6f45b2fc0f0" +dependencies = [ + "hashbrown 0.14.5", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + [[package]] name = "regex" version = "1.12.3" @@ -6276,6 +6684,12 @@ dependencies = [ "tokio-rustls 0.25.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.1" @@ -6305,6 +6719,19 @@ dependencies = [ "transpose", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -6314,7 +6741,7 @@ dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -7085,6 +7512,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "shlex" version = "1.3.0" @@ -7183,11 +7616,20 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -7299,6 +7741,12 @@ dependencies = [ "der", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7485,7 +7933,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.2", + "toml 0.8.23", "version-compare", ] @@ -7901,7 +8349,7 @@ dependencies = [ "fastrand", "getrandom 0.4.1", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -7916,6 +8364,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termtree" version = "0.5.1" @@ -8169,14 +8626,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.2" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -8211,9 +8668,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] @@ -8243,7 +8700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.6.3", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -8252,12 +8709,24 @@ name = "toml_edit" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.14", ] [[package]] @@ -8281,6 +8750,12 @@ dependencies = [ "winnow 1.0.2", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.1.1+spec-1.1.0" @@ -8982,6 +9457,15 @@ version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" +[[package]] +name = "wasm-encoder" +version = "0.217.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10961fd76db420582926af70816dd205019d8152d9e51e1b939125dd1639f854" +dependencies = [ + "leb128", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -8989,7 +9473,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +dependencies = [ + "leb128fmt", + "wasmparser 0.246.2", ] [[package]] @@ -9011,8 +9505,8 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] @@ -9028,6 +9522,41 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm3" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7dde97449e99be474a432bbb0b1ab40b8f7ce3e97aa7ac640e9ecd018bbf88" +dependencies = [ + "cty", + "wasm3-sys", +] + +[[package]] +name = "wasm3-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a4e5d10bf1ffe7753275d4bbae3dc135dd2d2decd90e615accf9fef8bc52bab" +dependencies = [ + "cc", + "cty", + "shlex 0.1.1", +] + +[[package]] +name = "wasmparser" +version = "0.217.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65a5a0689975b9fd93c02f5400cfd9669858b99607e54e7b892c6080cba598bb" +dependencies = [ + "ahash", + "bitflags 2.11.0", + "hashbrown 0.14.5", + "indexmap 2.13.0", + "semver", + "serde", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -9040,6 +9569,307 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags 2.11.0", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.217.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324c6782d7b81c01625335d252653b26ea68e835ddb4aef4cb1ed3ea40ae3a49" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.217.1", +] + +[[package]] +name = "wasmtime" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38dbf42dc56a6fe41ccd77211ea8ec90855de05e52cd00df5a0a3bca87d6147" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags 2.11.0", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "fxprof-processed-profile", + "gimli", + "hashbrown 0.14.5", + "indexmap 2.13.0", + "ittapi", + "libc", + "libm", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "paste", + "postcard", + "psm", + "rayon", + "rustix 0.38.44", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "sptr", + "target-lexicon", + "wasm-encoder 0.217.1", + "wasmparser 0.217.1", + "wasmtime-asm-macros", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "wat", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e0c7f9983c2d60109a939d9ab0e0df301901085c3608e1c22c27c98390a027" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52eaa50abc14a9a2550d05e99e5e72d43ba75ea99cac1a440b61f1b9b87cd11" +dependencies = [ + "anyhow", + "base64 0.21.7", + "directories-next", + "log", + "postcard", + "rustix 0.38.44", + "serde", + "serde_derive", + "sha2", + "toml 0.8.23", + "windows-sys 0.52.0", + "zstd 0.13.3", +] + +[[package]] +name = "wasmtime-component-macro" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0929ffffaca32dd8770b56848c94056036963ca05de25fb47cac644e20262168" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.217.1", +] + +[[package]] +name = "wasmtime-component-util" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc29d2b56629d66d2fd791d1b46471d0016e0d684ed2dc299e870d127082268" + +[[package]] +name = "wasmtime-cranelift" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c8af1197703f4de556a274384adf5db36a146f9892bc9607bad16881e75c80" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "gimli", + "log", + "object 0.36.7", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.217.1", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1b5af7bac868c5bce3b78a366a10677caacf6e6467c156301297e36ed31f3e" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap 2.13.0", + "log", + "object 0.36.7", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "target-lexicon", + "wasm-encoder 0.217.1", + "wasmparser 0.217.1", + "wasmprinter", + "wasmtime-component-util", + "wasmtime-types", +] + +[[package]] +name = "wasmtime-fiber" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665ccc1bb0f28496e6fa02e94c575ee9ad6e3202c7df8591e5dda78106d5aa4a" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "rustix 0.38.44", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106731c6ebe1d551362ee8c876d450bdc2d517988b20eb3653dc4837b1949437" +dependencies = [ + "object 0.36.7", + "once_cell", + "rustix 0.38.44", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7314e32c624f645ad7d6b9fc3ac89eb7d2b9aa06695d6445cec087958ec27d" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-slab" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75cba1a8cc327839f493cfc3036c9de3d077d59ab76296bc710ee5f95be5391" + +[[package]] +name = "wasmtime-types" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d83a7816947a4974e2380c311eacb1db009b8bad86081dc726b705603c93c7" +dependencies = [ + "anyhow", + "cranelift-entity", + "serde", + "serde_derive", + "smallvec", + "wasmparser 0.217.1", +] + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6879a8e168aef3fe07335343b7fbede12fa494215e83322e173d4018e124a846" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "wasmtime-winch" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6baca2a919a288df653246069868b4de80f07e9679a8ef9b78ad79fc658ffd12" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.217.1", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "25.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f571f63ac1d532e986eb3973bbef3a45e4ae83de521a8d573b0fe0594dc9608" +dependencies = [ + "anyhow", + "heck 0.4.1", + "indexmap 2.13.0", + "wit-parser 0.217.1", +] + +[[package]] +name = "wast" +version = "246.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.246.2", +] + +[[package]] +name = "wat" +version = "1.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.91" @@ -9480,7 +10310,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", - "toml 0.8.2", + "toml 0.8.23", "tracing", "tracing-subscriber", "walkdir", @@ -9559,6 +10389,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cd1dc56c5a45d509ff06e7ca8817eaa9ec3240096f07e71915d5d528658e8a" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "wasmparser 0.217.1", + "wasmtime-cranelift", + "wasmtime-environ", +] + [[package]] name = "window-vibrancy" version = "0.6.0" @@ -10151,7 +10998,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -10198,10 +11045,28 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasm-metadata", - "wasmparser", - "wit-parser", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.217.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5aaf02882453eaeec4fe30f1e4263cfd8b8ea36dd00e1fe7d902d9cb498bccd" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.217.1", ] [[package]] @@ -10219,7 +11084,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.244.0", ] [[package]] @@ -10301,7 +11166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -10457,7 +11322,7 @@ dependencies = [ "pbkdf2", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -10516,7 +11381,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe 7.2.4", ] [[package]] @@ -10529,6 +11403,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.16+zstd.1.5.7" diff --git a/v2/Cargo.toml b/v2/Cargo.toml index b6a11087..5af860d1 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -29,6 +29,8 @@ members = [ "crates/nvsim", "crates/nvsim-server", "crates/homecore", # ADR-127 — HOMECORE state machine + "crates/homecore-plugins", # ADR-128 — HOMECORE-PLUGINS WASM runtime (P1 scaffold) + "crates/homecore-api", # ADR-130 — HOMECORE REST + WS API # ADR-100/ADR-101: Cognitum Cog packaging — first Cog from this repo. # Ships the wifi-densepose pose-estimation model as a signed binary + # JSONL manifest installable by the Cognitum V0 appliance (cognitum-v0, diff --git a/v2/crates/homecore-plugins/Cargo.toml b/v2/crates/homecore-plugins/Cargo.toml new file mode 100644 index 00000000..ac90bec9 --- /dev/null +++ b/v2/crates/homecore-plugins/Cargo.toml @@ -0,0 +1,57 @@ +# HOMECORE-PLUGINS — WASM integration plugin system. +# Implements ADR-128 (HOMECORE-PLUGINS), P1 scaffold: +# - PluginManifest (serde-deserialised, superset of HA manifest.json) +# - HomeCorePlugin async trait + PluginId + PluginError +# - PluginRuntime trait + InProcessRuntime (native Rust, first-party plugins) +# - PluginRegistry (load / unload / list) +# +# P2 will add the `wasmtime` feature (gated below, default-off) for the real +# Wasmtime JIT sandbox. wasm3 interpretation mode lands behind `--features wasm3` +# in P3 for constrained-hardware targets. + +[package] +name = "homecore-plugins" +version = "0.1.0-alpha.0" +edition = "2021" +license = "MIT" +authors = ["rUv ", "HOMECORE Contributors"] +description = "WASM integration plugin runtime for HOMECORE (ADR-128 P1 scaffold)" +repository = "https://github.com/ruvnet/RuView" + +[lib] +name = "homecore_plugins" +path = "src/lib.rs" + +[features] +default = [] +# P2: real Wasmtime JIT sandbox (Cranelift; ~15 MB binary delta on Pi 5). +# Do not enable in production until the host ABI is frozen (ADR-128 §8 risk). +wasmtime = ["dep:wasmtime"] +# P3: wasm3 interpretation mode for constrained hardware (~50 kB). +wasm3 = ["dep:wasm3"] + +[dependencies] +# HOMECORE state machine — local path (ADR-127). +homecore = { path = "../homecore", version = "0.1.0-alpha.0" } + +# Async runtime — same version as workspace. +tokio = { version = "1", features = ["sync", "rt", "rt-multi-thread", "time", "macros"] } + +# Async trait support for HomeCorePlugin. +async-trait = "0.1" + +# Error handling. +thiserror = "1" + +# Serialisation (manifest JSON + ABI call payloads). +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Optional Wasmtime runtime (P2, default-off — 30 MB dep). +wasmtime = { version = "25", optional = true } + +# Optional wasm3 interpretation runtime (P3, default-off). +wasm3 = { version = "0.3", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["sync", "rt", "rt-multi-thread", "time", "macros", "test-util"] } diff --git a/v2/crates/homecore-plugins/src/error.rs b/v2/crates/homecore-plugins/src/error.rs new file mode 100644 index 00000000..6fe4bb9a --- /dev/null +++ b/v2/crates/homecore-plugins/src/error.rs @@ -0,0 +1,35 @@ +//! `PluginError` — typed error enum for the homecore-plugins crate. + +use thiserror::Error; + +/// Errors produced by the HOMECORE plugin system. +#[derive(Debug, Error)] +pub enum PluginError { + /// The plugin manifest JSON is missing required fields or is malformed. + #[error("invalid manifest: {0}")] + InvalidManifest(String), + + /// A plugin with this ID is already loaded in the registry. + #[error("plugin already loaded: {0}")] + AlreadyLoaded(String), + + /// No plugin with this ID is loaded in the registry. + #[error("plugin not found: {0}")] + NotFound(String), + + /// The plugin runtime failed to spawn or execute the plugin. + #[error("runtime error: {0}")] + RuntimeError(String), + + /// The plugin's `setup` hook returned an error. + #[error("plugin setup failed: {0}")] + SetupFailed(String), + + /// The plugin's `unload` hook returned an error. + #[error("plugin unload failed: {0}")] + UnloadFailed(String), + + /// IO error (manifest file not found, WASM binary missing, etc.). + #[error("io error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/v2/crates/homecore-plugins/src/lib.rs b/v2/crates/homecore-plugins/src/lib.rs new file mode 100644 index 00000000..41793964 --- /dev/null +++ b/v2/crates/homecore-plugins/src/lib.rs @@ -0,0 +1,51 @@ +//! HOMECORE-PLUGINS — WASM integration plugin system. +//! +//! Implements [ADR-128](../../docs/adr/ADR-128-homecore-integration-plugin-system.md) +//! P1 scaffold: manifest parsing, the `HomeCorePlugin` async trait, the +//! `PluginRuntime` abstraction, and the `PluginRegistry`. +//! +//! ## What's here (P1) +//! +//! - [`manifest`] — `PluginManifest`: superset of HA `manifest.json`; serde +//! round-trip + required-field validation. +//! - [`plugin`] — `HomeCorePlugin` async trait, `PluginId` newtype. +//! - [`runtime`] — `PluginRuntime` trait + `InProcessRuntime` (native Rust, +//! first-party plugins compiled into the binary). +//! - [`registry`] — `PluginRegistry`: load / unload / list plugins. +//! - [`error`] — `PluginError` typed error enum. +//! +//! ## What's NOT here yet (deferred) +//! +//! - `WasmtimeRuntime` (P2, `--features wasmtime`): Cranelift JIT sandbox on +//! Pi 5 / x86_64. The runtime-selection question (Wasmtime vs wasm3) is still +//! open (ADR-128 §8) and will be resolved in Q2 before P2 begins. +//! - Host ABI wiring: `hc_state_get`, `hc_state_set`, `hc_event_fire`, etc. +//! (P2 — requires ADR-127 state machine API freeze first). +//! - Config entry lifecycle + hot-load (P3). +//! - Cog registry distribution + Ed25519 signature verification (P4). +//! - Permission enforcement (P5). +//! +//! ## Feature flags +//! +//! | Feature | Default | Description | +//! |---------|---------|-------------| +//! | `wasmtime` | off | Wasmtime Cranelift JIT runtime (P2) | +//! | `wasm3` | off | wasm3 interpreter runtime for constrained hardware (P3) | + +pub mod error; +pub mod manifest; +pub mod plugin; +pub mod registry; +pub mod runtime; + +pub use error::PluginError; +pub use manifest::{IotClass, IntegrationType, PluginManifest}; +pub use plugin::{HomeCorePlugin, PluginId}; +pub use registry::PluginRegistry; +pub use runtime::{InProcessRuntime, LoadedPlugin, PluginRuntime}; + +#[cfg(feature = "wasmtime")] +pub use runtime::wasmtime_rt::WasmtimeRuntime; + +#[cfg(test)] +mod tests; diff --git a/v2/crates/homecore-plugins/src/manifest.rs b/v2/crates/homecore-plugins/src/manifest.rs new file mode 100644 index 00000000..d1082a7d --- /dev/null +++ b/v2/crates/homecore-plugins/src/manifest.rs @@ -0,0 +1,144 @@ +//! Plugin manifest — superset of HA's `manifest.json`. +//! +//! See ADR-128 §3 for the full field list. Fields present in HA's schema +//! are preserved verbatim. HOMECORE-specific fields are marked `[HOMECORE]`. + +use serde::{Deserialize, Serialize}; + +use crate::error::PluginError; + +/// Coarse-grained permission claim string (glob pattern). +/// Example: `"state:write:sensor.*"`. +pub type PermissionClaim = String; + +/// HA `iot_class` values (non-exhaustive — HA adds new classes over time). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IotClass { + LocalPush, + LocalPolling, + CloudPush, + CloudPolling, + AssumedState, + Calculated, + #[serde(other)] + Other, +} + +/// HOMECORE integration type. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IntegrationType { + Integration, + Helper, + Entity, + #[serde(other)] + Other, +} + +/// Parsed and validated plugin manifest. +/// +/// Serialises to/from HA-compatible `manifest.json`. HOMECORE-only fields +/// are `Option<…>` so that a plain HA manifest is a valid (native-only) +/// HOMECORE manifest. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PluginManifest { + /// Unique integration domain identifier (e.g. `"mqtt"`). + pub domain: String, + + /// Human-readable integration name. + pub name: String, + + /// SemVer-ish version string (HA uses calendar-versioning, e.g. `"2025.1.0"`). + pub version: String, + + /// Optional documentation URL. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub documentation: Option, + + /// HA `iot_class` — how the integration communicates with the device. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub iot_class: Option, + + /// Whether this integration ships a UI config flow. + #[serde(default)] + pub config_flow: bool, + + /// HOMECORE integration type (optional, defaults to Integration). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub integration_type: Option, + + /// Intra-HOMECORE dependencies (other plugin domains this one requires). + #[serde(default)] + pub dependencies: Vec, + + /// External package requirements — kept for schema compat, ignored in HOMECORE + /// (WASM modules carry their own static deps, no pip). + #[serde(default)] + pub requirements: Vec, + + // ── [HOMECORE] fields ────────────────────────────────────────────────── + + /// [HOMECORE] Relative path to the `.wasm` binary (absent for native plugins). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wasm_module: Option, + + /// [HOMECORE] `sha256:` hash of the wasm binary; verified before execution. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wasm_module_hash: Option, + + /// [HOMECORE] Ed25519 signature of the wasm binary hash (`ed25519:`). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wasm_module_sig: Option, + + /// [HOMECORE] Ed25519 public key of the plugin publisher. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub publisher_key: Option, + + /// [HOMECORE] Minimum HOMECORE version required by this plugin. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_homecore_version: Option, + + /// [HOMECORE] Subset of host functions the WASM module imports. + #[serde(default)] + pub host_imports_required: Vec, + + /// [HOMECORE] Coarse-grained permission claims (glob patterns). + #[serde(default)] + pub homecore_permissions: Vec, + + /// [HOMECORE] Seed app registry cog ID for distribution. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cog_id: Option, +} + +impl PluginManifest { + /// Parse a `manifest.json` JSON string and validate required fields. + /// + /// Required fields: `domain`, `name`, `version`. + pub fn parse_json(s: &str) -> Result { + let m: Self = serde_json::from_str(s) + .map_err(|e| PluginError::InvalidManifest(e.to_string()))?; + m.validate()?; + Ok(m) + } + + fn validate(&self) -> Result<(), PluginError> { + if self.domain.trim().is_empty() { + return Err(PluginError::InvalidManifest( + "manifest `domain` must not be empty".into(), + )); + } + if self.name.trim().is_empty() { + return Err(PluginError::InvalidManifest( + "manifest `name` must not be empty".into(), + )); + } + if self.version.trim().is_empty() { + return Err(PluginError::InvalidManifest( + "manifest `version` must not be empty".into(), + )); + } + Ok(()) + } +} diff --git a/v2/crates/homecore-plugins/src/plugin.rs b/v2/crates/homecore-plugins/src/plugin.rs new file mode 100644 index 00000000..634a72cd --- /dev/null +++ b/v2/crates/homecore-plugins/src/plugin.rs @@ -0,0 +1,59 @@ +//! `HomeCorePlugin` trait + `PluginId` newtype. +//! +//! Every first-party and third-party HOMECORE integration must implement +//! `HomeCorePlugin`. P1 provides an in-process native Rust implementation; +//! the WASM ABI wrapper (which maps the WASM exports `setup_entry`, +//! `call_service_handler`, `receive_event` to this trait) lands in P2. + +use std::fmt; + +use async_trait::async_trait; +use homecore::HomeCore; + +use crate::error::PluginError; + +/// Unique identifier for a loaded plugin — mirrors the `domain` field of +/// the plugin's `PluginManifest` (e.g. `"mqtt"`, `"homecore_lights"`). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PluginId(pub String); + +impl PluginId { + /// Create a new `PluginId` from any string-like value. + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + /// Return the inner domain string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for PluginId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +/// Lifecycle trait that every HOMECORE integration must implement. +/// +/// Implementing types are passed to [`PluginRuntime::load`]; the runtime +/// calls these methods at the appropriate lifecycle points. +/// +/// # Async +/// Both methods are `async` to allow network / IO initialisation without +/// blocking the Tokio runtime. The `async_trait` macro erases the `impl` +/// return type so it works in trait objects. +#[async_trait] +pub trait HomeCorePlugin: Send + Sync + 'static { + /// Called once when the plugin's config entry is being set up. + /// + /// The plugin receives a reference to the `HomeCore` runtime and should + /// register its entities, services, and event subscriptions here. + async fn setup(&self, hc: HomeCore) -> Result<(), PluginError>; + + /// Called when the plugin is being removed from the registry. + /// + /// The plugin should clean up subscriptions and deregister its entities. + async fn unload(&self) -> Result<(), PluginError>; +} diff --git a/v2/crates/homecore-plugins/src/registry.rs b/v2/crates/homecore-plugins/src/registry.rs new file mode 100644 index 00000000..28131d36 --- /dev/null +++ b/v2/crates/homecore-plugins/src/registry.rs @@ -0,0 +1,102 @@ +//! `PluginRegistry` — load, unload, and list HOMECORE plugins. +//! +//! The registry is runtime-agnostic: it accepts any type that implements +//! [`PluginRuntime`] and delegates load/unload to it. This allows swapping +//! the `InProcessRuntime` (P1) for a `WasmtimeRuntime` (P2) without +//! changing registry code. + +use std::collections::HashMap; +use std::sync::Arc; + +use homecore::HomeCore; +use tokio::sync::RwLock; + +use crate::error::PluginError; +use crate::manifest::PluginManifest; +use crate::plugin::{HomeCorePlugin, PluginId}; +use crate::runtime::{LoadedPlugin, PluginRuntime}; + +/// Holds all loaded plugins keyed by `PluginId`. +/// +/// Thread-safe via `RwLock` — concurrent reads are cheap; writes (load / +/// unload) take an exclusive lock only while mutating the map. +pub struct PluginRegistry { + runtime: R, + plugins: RwLock>, +} + +impl PluginRegistry { + /// Create an empty registry backed by `runtime`. + pub fn new(runtime: R) -> Self { + Self { + runtime, + plugins: RwLock::new(HashMap::new()), + } + } + + /// Load a plugin, call its `setup` hook, and insert it into the registry. + /// + /// Returns `PluginError::AlreadyLoaded` if a plugin with the same ID is + /// already registered. + pub async fn load( + &self, + manifest: PluginManifest, + plugin: Arc, + hc: HomeCore, + ) -> Result { + let id = PluginId::new(&manifest.domain); + + { + let guard = self.plugins.read().await; + if guard.contains_key(&id) { + return Err(PluginError::AlreadyLoaded(id.to_string())); + } + } + + let loaded = self + .runtime + .load(id.clone(), manifest, plugin) + .await?; + + loaded + .setup(hc) + .await + .map_err(|e| PluginError::SetupFailed(e.to_string()))?; + + self.plugins.write().await.insert(id.clone(), loaded); + Ok(id) + } + + /// Unload a plugin by ID, calling its `unload` hook first. + /// + /// Returns `PluginError::NotFound` if the plugin was not loaded. + pub async fn unload(&self, id: &PluginId) -> Result<(), PluginError> { + let loaded = { + let mut guard = self.plugins.write().await; + guard + .remove(id) + .ok_or_else(|| PluginError::NotFound(id.to_string()))? + }; + + loaded + .unload() + .await + .map_err(|e| PluginError::UnloadFailed(e.to_string()))?; + + Ok(()) + } + + /// Return a snapshot of currently loaded plugin IDs and their manifest domains. + pub async fn list(&self) -> Vec<(PluginId, String)> { + let guard = self.plugins.read().await; + guard + .iter() + .map(|(id, lp)| (id.clone(), lp.manifest.domain.clone())) + .collect() + } + + /// Return `true` if a plugin with this ID is loaded. + pub async fn contains(&self, id: &PluginId) -> bool { + self.plugins.read().await.contains_key(id) + } +} diff --git a/v2/crates/homecore-plugins/src/runtime.rs b/v2/crates/homecore-plugins/src/runtime.rs new file mode 100644 index 00000000..d0f34cbc --- /dev/null +++ b/v2/crates/homecore-plugins/src/runtime.rs @@ -0,0 +1,119 @@ +//! `PluginRuntime` trait + `InProcessRuntime` (P1). +//! +//! Abstracts over Wasmtime (P2, `--features wasmtime`) and native in-process +//! Rust plugins (P1, always-on). A third backend, wasm3 (P3), will provide +//! interpretation mode for constrained hardware. +//! +//! # Architecture +//! +//! ```text +//! PluginRegistry +//! │ +//! ▼ +//! PluginRuntime ◄─── InProcessRuntime (P1, native Rust, <1 µs call) +//! ◄─── WasmtimeRuntime (P2, Cranelift JIT, ~5 ms cold start) +//! ◄─── Wasm3Runtime (P3, interpreter, ~50 kB, Pi Zero) +//! ``` + +use std::sync::Arc; + +use async_trait::async_trait; +use homecore::HomeCore; + +use crate::error::PluginError; +use crate::manifest::PluginManifest; +use crate::plugin::{HomeCorePlugin, PluginId}; + +/// A loaded plugin handle — returned by [`PluginRuntime::load`]. +pub struct LoadedPlugin { + pub id: PluginId, + pub manifest: PluginManifest, + /// Underlying plugin instance (boxed trait object). + pub(crate) instance: Arc, +} + +impl LoadedPlugin { + /// Delegate to the inner plugin's `setup` method. + pub async fn setup(&self, hc: HomeCore) -> Result<(), PluginError> { + self.instance.setup(hc).await + } + + /// Delegate to the inner plugin's `unload` method. + pub async fn unload(&self) -> Result<(), PluginError> { + self.instance.unload().await + } +} + +/// Abstraction over the WASM (and native) plugin execution environment. +/// +/// P2 will supply a `WasmtimeRuntime` that compiles `.wasm` bytes with +/// Cranelift; P3 adds a `Wasm3Runtime` for constrained targets. Both will +/// implement this trait so the registry is runtime-agnostic. +#[async_trait] +pub trait PluginRuntime: Send + Sync + 'static { + /// Load a plugin from a boxed [`HomeCorePlugin`] implementation and a + /// parsed `PluginManifest`. Returns a `LoadedPlugin` handle. + async fn load( + &self, + id: PluginId, + manifest: PluginManifest, + plugin: Arc, + ) -> Result; +} + +/// Native in-process runtime — loads first-party Rust plugins directly. +/// +/// No WASM compilation; no sandbox. Intended for first-party plugins +/// (RuView MQTT bridge, presence sensor, etc.) that are compiled into the +/// HOMECORE binary and therefore trusted. Third-party / community plugins +/// must use the `WasmtimeRuntime` (P2) for isolation. +pub struct InProcessRuntime; + +#[async_trait] +impl PluginRuntime for InProcessRuntime { + async fn load( + &self, + id: PluginId, + manifest: PluginManifest, + plugin: Arc, + ) -> Result { + Ok(LoadedPlugin { + id, + manifest, + instance: plugin, + }) + } +} + +// ── Feature-gated Wasmtime stub (P2) ────────────────────────────────────── + +#[cfg(feature = "wasmtime")] +pub mod wasmtime_rt { + //! Wasmtime JIT runtime — P2 stub. + //! + //! This module intentionally does not compile to a usable runtime yet. + //! It exists so that `cargo check --features wasmtime` exercises the + //! dependency graph and catches obvious breakage early. + //! + //! Full implementation tracked in ADR-128 §7 P2. + + use super::*; + + /// Wasmtime-backed plugin runtime (Cranelift JIT on Pi 5 and x86_64). + /// Not yet implemented — P2 work. + pub struct WasmtimeRuntime; + + #[async_trait] + impl PluginRuntime for WasmtimeRuntime { + async fn load( + &self, + _id: PluginId, + _manifest: PluginManifest, + _plugin: Arc, + ) -> Result { + Err(PluginError::RuntimeError( + "WasmtimeRuntime is not yet implemented (ADR-128 P2)".into(), + )) + } + } +} diff --git a/v2/crates/homecore-plugins/src/tests.rs b/v2/crates/homecore-plugins/src/tests.rs new file mode 100644 index 00000000..de20cd50 --- /dev/null +++ b/v2/crates/homecore-plugins/src/tests.rs @@ -0,0 +1,233 @@ +//! Unit tests for homecore-plugins P1 scaffold. +//! +//! Covers: manifest parse + round-trip, manifest field validation, +//! PluginRegistry load/unload/list/duplicate, InProcessRuntime, +//! and PluginError variants. + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use async_trait::async_trait; + use homecore::HomeCore; + use tokio::sync::Mutex; + + use crate::error::PluginError; + use crate::manifest::PluginManifest; + use crate::plugin::{HomeCorePlugin, PluginId}; + use crate::registry::PluginRegistry; + use crate::runtime::InProcessRuntime; + + // ── Test double ──────────────────────────────────────────────────────── + + /// Minimal plugin that records setup/unload calls. + struct TestPlugin { + pub setup_called: Mutex, + pub unload_called: Mutex, + } + + impl TestPlugin { + fn new() -> Arc { + Arc::new(Self { + setup_called: Mutex::new(false), + unload_called: Mutex::new(false), + }) + } + } + + #[async_trait] + impl HomeCorePlugin for TestPlugin { + async fn setup(&self, _hc: HomeCore) -> Result<(), PluginError> { + *self.setup_called.lock().await = true; + Ok(()) + } + + async fn unload(&self) -> Result<(), PluginError> { + *self.unload_called.lock().await = true; + Ok(()) + } + } + + fn minimal_manifest(domain: &str) -> PluginManifest { + PluginManifest { + domain: domain.into(), + name: "Test Plugin".into(), + version: "1.0.0".into(), + documentation: None, + iot_class: None, + config_flow: false, + integration_type: None, + dependencies: vec![], + requirements: vec![], + wasm_module: None, + wasm_module_hash: None, + wasm_module_sig: None, + publisher_key: None, + min_homecore_version: None, + host_imports_required: vec![], + homecore_permissions: vec![], + cog_id: None, + } + } + + // ── Manifest tests ───────────────────────────────────────────────────── + + #[test] + fn manifest_parse_round_trip() { + let json = r#"{ + "domain": "mqtt", + "name": "MQTT", + "version": "2025.1.0", + "iot_class": "local_push", + "config_flow": true, + "dependencies": [], + "requirements": [], + "wasm_module": "mqtt.wasm", + "homecore_permissions": ["state:write:sensor.*"] + }"#; + + let m = PluginManifest::parse_json(json).expect("should parse"); + assert_eq!(m.domain, "mqtt"); + assert_eq!(m.version, "2025.1.0"); + assert!(m.config_flow); + assert_eq!(m.homecore_permissions, vec!["state:write:sensor.*"]); + + // round-trip: serialize back to JSON and re-parse + let serialised = serde_json::to_string(&m).expect("should serialise"); + let m2 = PluginManifest::parse_json(&serialised).expect("round-trip should parse"); + assert_eq!(m.domain, m2.domain); + assert_eq!(m.version, m2.version); + } + + #[test] + fn manifest_rejects_empty_domain() { + let json = r#"{"domain":"","name":"X","version":"1.0.0"}"#; + let err = PluginManifest::parse_json(json).unwrap_err(); + assert!( + err.to_string().contains("domain"), + "error should mention domain: {err}" + ); + } + + #[test] + fn manifest_rejects_missing_domain() { + let json = r#"{"name":"X","version":"1.0.0"}"#; + // serde will fill domain as "" due to missing field → validation rejects + let err = PluginManifest::parse_json(json).unwrap_err(); + // Either a serde error (missing field) or a validation error is acceptable + let s = err.to_string(); + assert!(!s.is_empty(), "should produce a non-empty error"); + } + + #[test] + fn manifest_rejects_empty_version() { + let json = r#"{"domain":"lights","name":"Lights","version":""}"#; + let err = PluginManifest::parse_json(json).unwrap_err(); + assert!( + err.to_string().contains("version"), + "error should mention version: {err}" + ); + } + + // ── Registry + InProcessRuntime tests ───────────────────────────────── + + #[tokio::test] + async fn registry_load_and_list() { + let hc = HomeCore::new(); + let registry = PluginRegistry::new(InProcessRuntime); + let plugin = TestPlugin::new(); + let manifest = minimal_manifest("lights"); + + let id = registry + .load(manifest, plugin.clone(), hc) + .await + .expect("load should succeed"); + + assert_eq!(id.as_str(), "lights"); + assert!(*plugin.setup_called.lock().await, "setup should have been called"); + + let listing = registry.list().await; + assert_eq!(listing.len(), 1); + assert_eq!(listing[0].0.as_str(), "lights"); + } + + #[tokio::test] + async fn registry_unload_removes_plugin() { + let hc = HomeCore::new(); + let registry = PluginRegistry::new(InProcessRuntime); + let plugin = TestPlugin::new(); + + let id = registry + .load(minimal_manifest("switch"), plugin.clone(), hc) + .await + .expect("load should succeed"); + + registry.unload(&id).await.expect("unload should succeed"); + assert!(*plugin.unload_called.lock().await, "unload should have been called"); + assert_eq!(registry.list().await.len(), 0); + } + + #[tokio::test] + async fn registry_rejects_duplicate_load() { + let hc1 = HomeCore::new(); + let hc2 = HomeCore::new(); + let registry = PluginRegistry::new(InProcessRuntime); + + registry + .load(minimal_manifest("sensor"), TestPlugin::new(), hc1) + .await + .expect("first load should succeed"); + + let err = registry + .load(minimal_manifest("sensor"), TestPlugin::new(), hc2) + .await + .unwrap_err(); + + assert!( + matches!(err, PluginError::AlreadyLoaded(_)), + "expected AlreadyLoaded, got: {err:?}" + ); + } + + #[tokio::test] + async fn registry_unload_unknown_plugin_returns_not_found() { + let registry = PluginRegistry::new(InProcessRuntime); + let id = PluginId::new("nonexistent"); + let err = registry.unload(&id).await.unwrap_err(); + assert!( + matches!(err, PluginError::NotFound(_)), + "expected NotFound, got: {err:?}" + ); + } + + #[tokio::test] + async fn in_process_runtime_setup_called() { + let hc = HomeCore::new(); + let registry = PluginRegistry::new(InProcessRuntime); + let plugin = TestPlugin::new(); + + registry + .load(minimal_manifest("climate"), plugin.clone(), hc) + .await + .expect("load should succeed"); + + assert!( + *plugin.setup_called.lock().await, + "InProcessRuntime must call setup" + ); + } + + // ── Error display ────────────────────────────────────────────────────── + + #[test] + fn error_display_variants() { + let e1 = PluginError::AlreadyLoaded("mqtt".into()); + assert!(e1.to_string().contains("mqtt")); + + let e2 = PluginError::NotFound("climate".into()); + assert!(e2.to_string().contains("climate")); + + let e3 = PluginError::RuntimeError("boom".into()); + assert!(e3.to_string().contains("boom")); + } +}