From 05c8a5d8f88137ca4c2605a01f6b1cdaf67721d7 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:50:14 -0700 Subject: [PATCH 1/2] Apps' own Rust functions in WebAssembly, behind the wasm-lab feature An app may ship its developer's own functions as WebAssembly modules in its bundle's fns/, and the shell runs them for that app in a sandbox (ADR 0011, proposed). Off by default: everything is behind the new `wasm-lab` feature, and default builds change only by the App Hub repin and the libc patch bump below. - crates/wasm-host: Wasmtime 49, compiled by Cranelift. Every call has a 2 s deadline (epochs advanced by wall-clock time), a 256 MiB memory cap, a 512 KiB wasm stack and 16 MiB of input and output; the only import is octo.log. A trap or a deadline spends the instance (a Rust guest's stack pointer and allocator may be half-updated), and the next call gets a fresh one. Compiled code is cached by the module's digest and the engine's compatibility hash; the writer reads the file back in the background, because Microsoft Defender's scanner holds the first open of a new file for about a second. - The `wasm` host service (crates/shell/src/wasm_service.rs): wasm. calls one of the calling app's functions, and wasm.functions lists them with load and per-call numbers. Modules come only from the app's own admitted bundle, the service checks the `wasm` grant itself, and each app has one worker thread, never the UI thread. An agent tool reaches a function with host_method "wasm.". - Wasm Lab (apps/wasmlab), a demo system app shipped only by the desktop/ and phone/system-apps-wasm-lab.json selections: fuzzy ranking as you type, free slots, a line diff, Markdown to HTML and four ways to misbehave, with three of them as its agent's tools. Its guest crate (apps/wasmlab/guest) builds the module. - App Hub -> 2e576896 (main with #140): app-contract 1.7.0 with the `wasm` capability, now from crates.io; the workspace asks for contract 1.7. - libc 0.2.189 -> 0.2.190, also required by crates/wasm-host: on aarch64 Android, 0.2.189's ucontext_t lacked the kernel's padding after the signal mask, Wasmtime misread a trap's pc, and every signal-based trap killed Home. Measured on an M5 Max and a Redmi Note 12: 1.4-2.2x native time for these functions, compile 27-33 ms / 378-421 ms, cached load 1-6 ms / 5-11 ms, about 2 ms from script to function and back, all four misbehaving modes contained on both, the APK 3.5 MiB larger. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 608 +++++++++++++++++- Cargo.toml | 17 +- apps/wasmlab/bundle/AGENT.md | 15 + apps/wasmlab/bundle/fns/wasmlab.wasm | Bin 0 -> 443650 bytes apps/wasmlab/bundle/icon.svg | 1 + apps/wasmlab/bundle/main.splash | 257 ++++++++ apps/wasmlab/bundle/manifest.json | 14 + apps/wasmlab/bundle/tools.json | 199 ++++++ apps/wasmlab/guest/Cargo.lock | 159 +++++ apps/wasmlab/guest/Cargo.toml | 12 + apps/wasmlab/guest/build.sh | 9 + apps/wasmlab/guest/functions/Cargo.toml | 16 + apps/wasmlab/guest/functions/src/lib.rs | 358 +++++++++++ apps/wasmlab/guest/octosense-guest/Cargo.toml | 9 + apps/wasmlab/guest/octosense-guest/src/lib.rs | 164 +++++ crates/shell/Cargo.toml | 5 + crates/shell/src/apps.rs | 3 + crates/shell/src/host_tools/script_apps.rs | 2 +- crates/shell/src/lib.rs | 2 + crates/shell/src/wasm_service.rs | 425 ++++++++++++ crates/wasm-host/Cargo.toml | 21 + crates/wasm-host/examples/measure.rs | 113 ++++ crates/wasm-host/src/lib.rs | 570 ++++++++++++++++ crates/wasm-host/tests/guest.rs | 170 +++++ crates/wasm-host/tests/runtime.rs | 255 ++++++++ desktop/Cargo.toml | 2 + desktop/system-apps-wasm-lab.json | 15 + native-apps.json | 2 +- phone/Cargo.toml | 2 + phone/system-apps-wasm-lab.json | 20 + tools/sync-app-interface.py | 2 +- 31 files changed, 3426 insertions(+), 21 deletions(-) create mode 100644 apps/wasmlab/bundle/AGENT.md create mode 100755 apps/wasmlab/bundle/fns/wasmlab.wasm create mode 100644 apps/wasmlab/bundle/icon.svg create mode 100644 apps/wasmlab/bundle/main.splash create mode 100644 apps/wasmlab/bundle/manifest.json create mode 100644 apps/wasmlab/bundle/tools.json create mode 100644 apps/wasmlab/guest/Cargo.lock create mode 100644 apps/wasmlab/guest/Cargo.toml create mode 100755 apps/wasmlab/guest/build.sh create mode 100644 apps/wasmlab/guest/functions/Cargo.toml create mode 100644 apps/wasmlab/guest/functions/src/lib.rs create mode 100644 apps/wasmlab/guest/octosense-guest/Cargo.toml create mode 100644 apps/wasmlab/guest/octosense-guest/src/lib.rs create mode 100644 crates/shell/src/wasm_service.rs create mode 100644 crates/wasm-host/Cargo.toml create mode 100644 crates/wasm-host/examples/measure.rs create mode 100644 crates/wasm-host/src/lib.rs create mode 100644 crates/wasm-host/tests/guest.rs create mode 100644 crates/wasm-host/tests/runtime.rs create mode 100644 desktop/system-apps-wasm-lab.json create mode 100644 phone/system-apps-wasm-lab.json diff --git a/Cargo.lock b/Cargo.lock index c8706702..eeea0f35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,16 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", +] + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli 0.33.0", ] [[package]] @@ -716,11 +725,11 @@ version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ - "addr2line", + "addr2line 0.25.1", "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "libc", "miniz_oxide 0.8.9", - "object", + "object 0.37.3", "rustc-demangle", "windows-link 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -935,6 +944,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytecount" @@ -1333,6 +1345,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -1521,6 +1542,15 @@ dependencies = [ "libm", ] +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cpu-time" version = "1.0.0" @@ -1555,6 +1585,152 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658e8623bea025d142fb11c299b8c6572ffe9fd663b52887c4dc124735cc130b" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b12b8c55c31c3d8598c2de9a657c3b978f905f6886c43e02216e0b15fe45a3" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894b984f82c5a36f05fdd4973fbf4d37d69cd5cdf8c16e8ffe3af4a9cdfe193c" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1e4c34abe94193ada09f821f474be87ac8a1fbbacd87058571ef04c1bf4009" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63826cef0bf70e61d56cc930aa4ee423638f66dc3cfc10d683e73138a6d177b8" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.33.0", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash 2.1.3", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec 1.16.1", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26de7849112293681a19f7525c689e1d73dc6eb2687355170fb7458c4da8d353" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df315a8b1cfd6e620971e3d4249bd2c4528167717e8f48a6317a87c55891bcb8" + +[[package]] +name = "cranelift-control" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3fff7610fb17f228f16598e1a044db79a12806ece7496ff2c69cf1a27a79a5" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6158b1ac381792ad11e670abbcd15db8b1737e84bfa8a7f2bd7eb41388a92530" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099361a0919067086dcde624a281ce719834cebf8544d6c24646f5e924107234" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec 1.16.1", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2f30ea3fd5a11f7d3473976d3b62b5b3f2b62fa943c85e0331d88bf505082c" + +[[package]] +name = "cranelift-native" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397508a8d817dc74019603ac5a4c4e40b4df627e5aed4f2f8088345d4c01d401" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.136.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4944ef05513e1c1a85a945cf8f29d95ddea8bcbb19e0bfd4e71063450967420" + [[package]] name = "crc" version = "3.4.0" @@ -2288,6 +2464,18 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +[[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 = "emojis" version = "0.9.0" @@ -2939,7 +3127,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", + "windows-link 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "windows-result 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -3029,6 +3217,18 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[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 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 2.14.2", + "stable_deref_trait", +] + [[package]] name = "gix" version = "0.79.0" @@ -3897,6 +4097,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "foldhash 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_core", ] [[package]] @@ -4885,6 +5087,12 @@ dependencies = [ "goblin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lettre" version = "0.11.23" @@ -4914,9 +5122,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libdbus-sys" @@ -5118,6 +5326,12 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + [[package]] name = "macroific" version = "2.0.0" @@ -6398,6 +6612,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + [[package]] name = "memmap2" version = "0.9.11" @@ -6542,7 +6765,7 @@ dependencies = [ "bitflags 2.13.2", "combine", "libc", - "mach2", + "mach2 0.4.3", "nix 0.30.1", "sysctl", "thiserror 2.0.20", @@ -7034,6 +7257,18 @@ dependencies = [ "memchr 2.8.3", ] +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap 2.14.2", + "memchr 2.8.3", +] + [[package]] name = "octos-agent" version = "2.0.3-rc.13" @@ -7636,9 +7871,9 @@ dependencies = [ [[package]] name = "octosense-app-contract" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec46b54da6c411fce07e863af8ad36bb8394319deea861219e2815ad94e04f91" +checksum = "5c3e8bb885e67a1290abdf6d3a71bf59b2707fa7e67c9b833eae6843355e86e5" dependencies = [ "blake3", "serde", @@ -7649,7 +7884,7 @@ dependencies = [ [[package]] name = "octosense-app-hub" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=b1109a397ae45349546d50889a1706f6c58fb923#b1109a397ae45349546d50889a1706f6c58fb923" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=2e5768964f15916cdb66cc6824f7671dafc6445a#2e5768964f15916cdb66cc6824f7671dafc6445a" dependencies = [ "base64 0.22.1", "blake3", @@ -7668,7 +7903,7 @@ dependencies = [ [[package]] name = "octosense-app-hub-app" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=b1109a397ae45349546d50889a1706f6c58fb923#b1109a397ae45349546d50889a1706f6c58fb923" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=2e5768964f15916cdb66cc6824f7671dafc6445a#2e5768964f15916cdb66cc6824f7671dafc6445a" dependencies = [ "makepad-app-module", "makepad-widgets", @@ -7696,7 +7931,7 @@ dependencies = [ [[package]] name = "octosense-app-policy" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=b1109a397ae45349546d50889a1706f6c58fb923#b1109a397ae45349546d50889a1706f6c58fb923" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=2e5768964f15916cdb66cc6824f7671dafc6445a#2e5768964f15916cdb66cc6824f7671dafc6445a" dependencies = [ "makepad-widgets", "octosense-app-contract", @@ -7718,7 +7953,7 @@ dependencies = [ [[package]] name = "octosense-appstore" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=b1109a397ae45349546d50889a1706f6c58fb923#b1109a397ae45349546d50889a1706f6c58fb923" +source = "git+https://github.com/OctoSense-org/OctoSense-App-Hub?rev=2e5768964f15916cdb66cc6824f7671dafc6445a#2e5768964f15916cdb66cc6824f7671dafc6445a" dependencies = [ "makepad-app-module", "makepad-widgets", @@ -7740,6 +7975,13 @@ dependencies = [ "serde_json", ] +[[package]] +name = "octosense-guest" +version = "0.1.0" +dependencies = [ + "serde_json", +] + [[package]] name = "octosense-home" version = "0.1.0" @@ -7949,6 +8191,7 @@ dependencies = [ "octosense-news-service", "octosense-oauth-service", "octosense-reference", + "octosense-wasm-host", "rinx", "serde", "serde_json", @@ -7979,6 +8222,18 @@ dependencies = [ "url", ] +[[package]] +name = "octosense-wasm-host" +version = "0.1.0" +dependencies = [ + "libc", + "serde_json", + "sha2 0.10.9", + "wasmlab-functions", + "wasmtime", + "wat", +] + [[package]] name = "ohos-sys" version = "0.2.2" @@ -8475,6 +8730,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 = "postscript" version = "0.14.1" @@ -8603,6 +8870,29 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pulley-interpreter" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b66b7448cb92d709f2924de98b5ba5b5ce4ea08e6d6b9c3b403fcd88d122ca2" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185b9b4cd91f85110f48f8c0314757dfb2bb48c40f5d91055ea666cbbcbf317b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pxfm" version = "0.1.30" @@ -8967,6 +9257,21 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash 2.1.3", + "serde", + "smallvec 1.16.1", +] + [[package]] name = "regex" version = "1.13.1" @@ -10224,6 +10529,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -10920,6 +11229,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -10953,6 +11268,15 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -12084,6 +12408,26 @@ dependencies = [ "unicode-ident 1.0.24", ] +[[package]] +name = "wasm-encoder" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" +dependencies = [ + "leb128fmt", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasm-encoder" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser 0.261.0", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -12128,6 +12472,244 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmlab-functions" +version = "0.1.0" +dependencies = [ + "octosense-guest", + "pulldown-cmark 0.13.4", + "serde", + "similar", + "strsim", +] + +[[package]] +name = "wasmparser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" +dependencies = [ + "bitflags 2.13.2", + "hashbrown 0.17.1", + "indexmap 2.14.2", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags 2.13.2", + "indexmap 2.14.2", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasmtime" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1801b9c9f91e16e352ed77656824e7c4556a7538dbfc1a50b37243f0e12ea045" +dependencies = [ + "addr2line 0.26.1", + "async-trait", + "bitflags 2.13.2", + "bumpalo", + "cc", + "futures", + "libc", + "log", + "mach2 0.6.0", + "memfd", + "object 0.40.0", + "once_cell 1.21.4", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "serde", + "serde_derive", + "smallvec 1.16.1", + "target-lexicon", + "wasmparser 0.258.3", + "wasmtime-environ", + "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", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f397969b8daf208e45120036be74956e99364ce916d5b5d67f9e9f0fe40e5477" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.33.0", + "hashbrown 0.17.1", + "indexmap 2.14.2", + "log", + "object 0.40.0", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec 1.16.1", + "target-lexicon", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fe14bd5ea6444a09f6baa3865559c93c91158930e7fe6d127429a9372f6165" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2053fd1d90080e502ca4ea39d86c3528587a40220b9572a874c8b742c219a00c" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e96d6c55401e68e40e3f74078ec96f92fe9b8a7e90e885fb87265e69eea559" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.33.0", + "itertools 0.14.0", + "log", + "object 0.40.0", + "pulley-interpreter", + "smallvec 1.16.1", + "target-lexicon", + "thiserror 2.0.20", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b913875bab1b78fada489e55d5e0a10b53286fb4bb09f5657212f549c9af36c" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0356a1a09dee1239904e09e962456d9f6edfa5da61c5e3e1b0e4669710f48fba" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dead9a2d7b5d835698a2e1a32006e7d0b5711f4697b24bab1564bb2488e3d7a" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ffac8fc893de2958b263d9d7274cf4c531ba9ff582558348d4e28d97b8a0f2" +dependencies = [ + "cranelift-codegen", + "log", + "object 0.40.0", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a20f0dcb6c6a006363d3f85b5c633675730487b0dc23726b2d40495872407fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wast" +version = "261.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "776443145731a4062e5b0d392892a2005909b6ab72d9fdc3cad53dd1a714e44a" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr 2.8.3", + "unicode-width", + "wasm-encoder 0.261.0", +] + +[[package]] +name = "wat" +version = "1.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b4d1a49ea73a8f3326e74e3a05db667001b16bd1035ed3356fc1a0ed05ca7f" +dependencies = [ + "wast", +] + [[package]] name = "wayland-backend" version = "0.3.12" diff --git a/Cargo.toml b/Cargo.toml index 1bec3169..51d51847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "crates/l0-chat", "crates/oauth-service", "crates/markdown-editor", + "crates/wasm-host", "apps/reference", "apps/news/host-service", "apps/mail/host-service", @@ -49,6 +50,7 @@ exclude = [ "apps/appcard/tools", "apps/appcard/vendor", "apps/appcard/app/splash-native", + "apps/wasmlab/guest", ] default-members = ["desktop"] @@ -71,7 +73,7 @@ octosense-reference = { path = "apps/reference" } makepad-sheets = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false } makepad-terminal = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2" } octosense-appcard = { path = "apps/appcard/module" } -octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" } +octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" } makepad-calculator = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false } makepad-clock = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false } makepad-notes = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false } @@ -106,17 +108,17 @@ octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepa octoscript-node = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "33dea2f1f3ad3f1346a219aa8cf6e91b31361e23" } # --- App Hub's host service registry the Mail and llm services register # into; the same revision as App Hub's app crate (a native app, above). -octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" } +octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" } # The app policy (resolution, isolate settings, the Splash adapter) and the # Hub client the Card runner resolves an app's policy with; the shell's # glance tiles run cards under it (crates/shell/src/glance_card.rs). -octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" } -octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" } +octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" } +octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" } # The app contract (ADR 0005), from crates.io by version: App Hub, the shell # and Rinx all depend on `octosense-app-contract = "1"`, so Cargo links ONE # 1.x for the whole build (Cargo.lock pins the exact version OctoSense ships) # and App Hub and Rinx no longer need the same App Hub commit. -octosense-app-contract = "1.6" +octosense-app-contract = "1.7" # --- octos: ONE source for every octos crate (reviewed octos#2715 at # b0759a57: bounded ledger session-directory names, serialized peer purge # against session writes, and provider transport diagnostics that omit @@ -165,6 +167,7 @@ octosense-toolbox = { path = "crates/toolbox" } octosense-l0-chat = { path = "crates/l0-chat" } octosense-oauth-service = { path = "crates/oauth-service" } octosense-markdown-editor = { path = "crates/markdown-editor" } +octosense-wasm-host = { path = "crates/wasm-host" } article-core = { git = "https://github.com/hagency-org/Rinx.git", tag = "v1.1.0" } article-makepad = { git = "https://github.com/hagency-org/Rinx.git", tag = "v1.1.0" } octosense-llm-config = { path = "apps/ai-providers/config" } @@ -194,6 +197,10 @@ keyring = "3" # process with them (OctoSense hosting Rinx). rusqlite = { version = "0.40.1", features = ["bundled"] } log = "0.4" +# Developers' own functions compiled to WebAssembly, run for their app with +# a deadline and a memory cap (crates/wasm-host, ADR 0011). Cranelift compiles; +# no component model, async, GC or threads. +wasmtime = { version = "49.0.2", default-features = false, features = ["cranelift", "runtime", "std", "parallel-compilation"] } # Workspace-wide clippy policy for the crates that opt in (`[lints] # workspace = true`, AppCard's): lints that are real but tracked elsewhere. diff --git a/apps/wasmlab/bundle/AGENT.md b/apps/wasmlab/bundle/AGENT.md new file mode 100644 index 00000000..3be12100 --- /dev/null +++ b/apps/wasmlab/bundle/AGENT.md @@ -0,0 +1,15 @@ +# Wasm Lab + +Wasm Lab's tools are its own Rust functions, run on this device in a sandbox: +they compute an answer from what you give them and reach nothing else (no +files, network, calendar or other app). + +- wasmlab.find_slots: free slots of a given length in a day, around busy + intervals you pass ("HH:MM" pairs). It does not read anyone's calendar; + get the busy times first, from the person or from a tool that has them. +- wasmlab.rank: rank names against a query, best first, tolerating typos. +- wasmlab.diff: a unified line diff of two texts, with added and removed + counts. + +Report what a tool returned. When a tool says the input is wrong (a bad time, +a day that ends before it starts), fix the input or ask; do not guess. diff --git a/apps/wasmlab/bundle/fns/wasmlab.wasm b/apps/wasmlab/bundle/fns/wasmlab.wasm new file mode 100755 index 0000000000000000000000000000000000000000..cd1faeaa60f1e1082e38c1a9e89375ec6d90adbe GIT binary patch literal 443650 zcmeFa3%p&|UEjH1=Y7sSN79var7PL%Y+u_~wq!}REGdo|wDoap3x(Q{%zVOp_%von zLO54>C>Cy@LQ=4mI>VzWg~1fsdRoBF!(iU#E&54&)bmBmp%YVB-9DX`l=x*Ij)h&NgWP_|>(Qr{L@^gV61TFc9=)-dL3-M{jd z&}9E&dK`o1sHLhGsD}r`#byL%BQ6@l#;z>lCa1hWQBd7%q!E9>&{(7|Jct`1B0%`Zi*bHk z)`;I8zw@2cA8l@S;LhU0g-cPd^|#y2x4iK!Z+pi#CQ(>K2U9bfa-wdk9=Z+XM(&%EvRZ$9&uQ_oilHI=b6{P;p^Y@rs#aGQ zdi-ng55-^i;F+&|^Z%Kgddpki_V&~7IDTyO#=$rIFKb`3Y4Y_CeQW$}@&6kCK>UID zpT|EK|5*I1@wa3ji~m*pTk*%^KZ-vU|8Vjj;-5_ZP4cgkUrqjP@@(>J$w!lam3%UJ zF8Q70L&^VW zYx19x?@qrf{YU9{rvD)Q`{{er|D1eB`dIos>G!7Jo_<^UPtvE-52jD2A4vZ&{degv zroWs1Ui!K8Bk5<;hqLY{-@o|o>_4>P_WA2B@GmOdTAr@hX>v^exnvL((HJr6xo96y@zpd8df(9Yn6ZFS_*E4_}A|agT~J*Sc>Ig<7*$ z@w`(UyLS+~*kzNwQB?fW`Bx9(e$wOqFP;A~UgAlj*NV@*@6v^6^ofT7Fj7@5m#yVp z*8m>We)Zcl1J6%~3LQor$&Qx_{oKE_s9IuzX679;@YlN0>E1n zT6ay5?3?`lKb>pxaozYLH3?*Cas3@phePzrs2fm_J(NS`)#Mr9G?dfx)#NAax&z6e z^&Kki+Qsm6PA^VSMksjf{i4=~J+)j4Q9@z0)SvLvnH1^hU#NgfSCc+Gz+qG(Sr?06 zq~5|^0`bu~5Ys)$=t#O$Jfr)?&49QxDW0^8Pg&7F2)#*M^PYk6U{=WmD9Bvmd!O9x zS$Kx0OU8nk*XS2mjPj;igoVMqj5u0$6Hhy_=nq>YXlw+EM%`53#Emd zc;OwE!jlxAt_#moaU7STK3dBgzBD~ZTs+BRmq6%(|3;684mS$8)gVrE$Rv|Rd?u&7 zOHMB5NzpDcmlg5d%X;0#x(Cs-(FY`h)GVt-s zFM9tyi#z8N6HCEZq1+(yswg>zA`)iDO+d;f;}*&Zapi7`qG(U@4xB1(h0U5UEB$QG z(Xat_y$1TSLDp{sdeLI0_nU?lF*23pT~l(MqH&V?GS@sg;6ag|$$P8Gr%hLM9H*bM z)4oH|CDY22e%b?wqEDRm%LUC5TQ0eUzDS_#5q&FL$7AF zp6Q*o^bS>VoN69W5;PArjeAmo*2@+;$YUCTMM~COJE*l5{hO=4CC2QSTH`lVjvnp1 zPN~*-%d0gZ9Qn9!fLBLzViM3+{DAd0=ya~JBiwe$T5zVEFH zxOIIu;7X~P_8suVA655_Ip}mvRQ|+dJ;3y)8A(Y>NdcdL)>p5iaclxww5b}+HhSN? zV0i~OLmmC}N8hJAHo}#)lI}&$wQNM{!e5=h5}&0cl9tR$im z^~?(@xA%@4R;8qBHCY+Ac*@C9h``~1#5pbwOh!NAyTrXi$kyViNe(w0PWoV>DXSJa z7z+>zTHqCvzr)E0*DTh zY!01ubjaJD5~vmGeo>l^sKf|^)%I1Ix{eC2>ZJW_5;)p3z7gJfnOHBOqqv7Veb4G< z0mShQ&nC13XVGtp9H8GEj*escjo~?Ma<`i7@~{#EUF$1B{^(6viO6QmPNQ{j5dnBbUBtODY2xH4 z@NqEAtIsnXNUQ@U1d;_nhD=_X;v(2fn(&bKeqzw*oKMfC=aX}lfyb9`Ox0GYPSicqj7c^LCLwgcsjB#bx`%a*kL1kUVUT*i#DgXU2bcVgOa?>gV|v zpTt0)7hca4t+y8ECtvpv{=r}JBsw{02p^Mi(nq2jxL0@x-)|lurfe6gkHgZq*Kbt@ z3wZoU!?R5X@o1ecqI&=UZv$XBoE~FwtcCRSc!1$gQgg-xWM@wrbWrO!u?oIR4NB6$f{@q!dMAPJLxfS&zL5s z-2u)?alu4Oteu@%E8adCZxT?&`3Je4j5nKC)N&&?bel$x%8Wdbr>FDIxbN$k(ERZW zC9cjt4Emz4gr@Cr+ffMI_JIk9ZnAOV#oFEuo`9JAr;6DM;Rr!&tWxcm*v}sBQ?r>4y#eWEYA(qr~?G+ zxq*kPAK$BU-|tZK#nN9DV<^%~KlP1ntdi4Wzp}bk`03ychM<1wc7J z$h_p>obbxe=bWt?c*wTB!Vin}L#+l;sn9(bvE8)QsFrlj#Zz5 zN0OA!^Bnv=t@A4+$n_f+#|z>{DDSqqMb|A^Ihh3M%F|MaHlNN1>x5uf3ePsc7`Sbd zF+YN48+8s`f5Uf7Z~)`{+40C4AA0#2;9Tp4N`9o?YS2IIJv1?KnfB<`W_y~E;^e>$K89*=FN+FOC^Xk(ec51Ty!yy{lnv*k1yI=Expy^ zz3d|RjgN17PgUlunrNy5Tq&!7N3K!UK5epk&t>Jq>6=x<^*7b1+A@d)8E(p1YFG1| z(?(1WIK9~W(KtzPN&<KlY>$8=<)dB`_<_fr@MipbvRjc&4CSjP%u(5u6uIXHJ9@dHS>3J86%GWiuraU z=~4Zss!^;RbvWXcrj%wkI0Rc>nLGgx{s0Swo^Im1(J2C~yoJT(4E3y+fel|x zhHQ)KSu87|_(YG1eZC45V9scuk-(#e zyYN{Si0p*-!HHQ=1_X zoZ+h9xM*<`k=r%QEUXm=7z;Iv*NT2HLbLel35qVakaV+noi60~2Zasj&EkQo?3?Rl z-!NC!HDs#V_R1-}!jynqF6y&}&eUIi`~1tuz{66P;=I)8lK?7O3TyI;U`?RO{VrZB z?l&)F3L^KhIu)kRR+xVKOT+Z@=Uy77rx>UsT!YUh9(alHch>P>NU0%Qx?mm;D8j{4 z#qy+h=jlA2+E#k~cCb-1uVUKoipQyExW{5|gVS+rJQaHY#CxK+`BO15T~VB%+Z3G& zV3lPT>t#n9mEBd9U8$FKb7kSdRbe#~fr*&+G>vK0B4tL=*i1B^h|7yFbMehIMc9;vuuQry zT7rGO1jR@YzK9FB!nV+6Niy?N#tHbO2(e(PndGX7zF`OC0QvT$+vtDzP77bOT-U}+p4omP5Trv@E7PL>VdyL*K{MmW zR=nsRAMma4SP>=S8f6gIKR#AHzH6h$ABt-V>Z5U`p)LW@OECst92F&s2TM>YrkYK= zI^A?Bo>fjBO@C6y!=F#_@R`5qc&HKl>hSQH4e{{lsq65^60cI0J(fP2)VSq0lZsoO z<*k>(Ef>yz!3NFzhxknKnfUbD7iP{hL^QL$V`|o)1qX^!)r?v1+u}I0>5$Aqz6f61 zu;S3srYTK9nYK%V8Dn;FYQ{9fEQSsxyz!crz|}yTFzAy(N~Q)W^4Mk^SJbo#j%2Js z08q^8iL$}oue1PH9{KEljxh;u6q5-)SX^v7xxCPc+Dzb(nbRs04g4!SK0jI5%IxG| zFi7}I`LlmnV_@cTUzE+51fU7ZPuQukk>DgU;{jgsJ-UoZF!YpuKn*>?cupNMU)gA? zZ0jb0_FUQC%1ddF+Dns=1(Sf6z&&pg0+j2B7)-(xO7(nK&S;WQl8b62WLN_!O8h}j z0wk0*{MlIzf1&K4Ta~c0xe|(!GeGF5(99!YilzX+Y{1THWdb!vz6f?c3=Nnjs#sWc z>>4mQW3opD?95FI3`>@HS=zD56>G=NX4>(+u~`^Hmz~vmQ=+R;_1DfeC}DFSR)?K! zP$I4Bu(J(H*hGodVP_kZh^sp6Y=aUnK$+f$q<1{CZ!GHmnL%LnF#T5}n$2POB5qC) zDs8l0+;~CkUn}wAtnM2L%p)UmRr=~h724w-57JG-XXF#1blMg>8fxV!1r2D^U2uJo z;&6phyk$xu^f#YsY{IacEcjzT;$)s>!Ivadz*QME6k;l83z42qpI?gOq)cI9hO6bn zN%+q=%LE?tHkLX)xy&W8Hhs{aXitsVP`{jH=DkM{D4O-J#Kksq2rdb-IQJMtcK4<-W=HsBq3mXPXrc>eEX%;O8E=0UD{Lfer%#Q9U8J zC+FKO_{o9ZAW@~7Ct%h|e}r*{#pizJQY`_7&=vus5l=Q3kN#}!iRZ$*2vEa;cN&is zch=`giLBc^Xrgf#QH^qgoA* zhTKVUcA^=F3Y7z)((r4KhDH=nDiV#mVB!3OuIUlOSt{Jr(Y#3*0lkzYwF{s(3B?1$ zFFjrmhKKwpjM5u-HH)&~LFnTwfbXHMhIEUofoL;D`)J%1*C;eeRVgQ$@j03IRhO74 zsT7Vrr;#KDq=d;y$ zgj+p*3K?%#)dht23uW`%B_@H9ipT2WBc3XrtlyzA1s<+~*j-O-#7m?Y%*a(Bg$Pka z&z~nFzSM5$=h$nv(4cudLa15sS1Z83n%rx1cf>#;0+$N9TlfrZdzkzhg+G(F=5Oqb zSptO{H6$ASf)qK9<7rQ4?2AOuejB6j6CaHJiGB%)QYmO}sDZn3B|RV|K!YTHVGV22 zAvAea4WnolPx&O5V+I1CjV=mUbsr@~0mTqCMiJ1CJ3#C6wCGE)w|hM%S3M?Ilr^T( zM@jGCuyJOIS{5o`AoQhYQf8$@S+cu5lPV!k*+Bpm^3A693ty>jfrI^m2Q(j_Dl@z5 zGPA&>Is41@j0Nrn3oNd2<;9!@#){kH&ggp#DbeUL9qAszJIXCRg0n?tA8TgTKPCxX zjK*VfC+d%>vdoa}eWDZhNVGlNO4!rlm8FIGRw^LlB}-BEVmZduxrAqVW}+# zg)F2q*lXsk=^QR5evDlV>B?4h2aSP7n#e6}bIdg$bWPb(nh2L9o|=XYxU8@$+q!k! zM{Ppt%Hq1zBUJ?TPw9@eKh$C9w$i8ZR>Lt71drp*q^$seW&n+C`r?emllcOb(DlmX z+Bw7nTu#?YS-lIF^|E;OIhAyO2G5I<)rB$(OgYW60A_M&c@5JBH(+vuHn^e|AfYH} z8FC9Qv=B^I*#hiCVu?4oi}-TpE7KX)U5x%w5KoeyXUy)R(xKac1+Up;vovvemnVYi zoZ^4BI*)v-$9Qw5y2{_F%&%v^ISXtzlXXT|3B+Y@rp?ROnCVbSe)2Cw9HdSD1a47H z%mQ&ln#u_OWjeE@CtiU#7s>|3+~(Do5q_;V9rrUe2~uAzGb5ASM8KGvfH+(-^f7sF zS0=e#nPkph&?H;NgK5I@tTp<_f@btj^*d(PI0Wr!2u?NAEdBij0}xDe*(}LHu$VlA zD+B-qgo!rl95Y-HBnc)ObfD2FfFW&9O*92c6HT99%tTww8=GiP_m(%&WRQsNta=jN z8R&XO_H^IaME}?@zTM)I8ngD5F*(63bKcH-TFavWYThTGGdV+ulKLbCDru$z^QMvv z{7}#;NlsC{q+ELqb+BsKHH_J2Zh+i1tQzKp8;fDQoo_|SQ#XoJ+J3W?(>CR8ZueuX zB^xOz6JiIN+cZD}gSvBVysQyZKi{PSv^B|hlUU%*DFv=s8(&Md0E0P4rO9MG_Jg4u z3(uJ7Ivcj-D;g$U@0Q%o$@m6$jZE;3ZYRl0<5joIT{9W)al7f44dn8hY$$iLAE@nB z1?}-Zc<`3=7`?kC-;cs}`=$PGar?&y#GOjSa0gK3D6|8tSS#Z3LDwE1kex%RWM8JA zd)MTkE+lb;_9Ss{V(%WbQJ6P~(_fVr*L2BQ7~49A>vSgbw*T${aTf>m7}F`Gp`~+; zT9krd^R6UghSU27WZ6~)%DdxRm3+(X;&VT83Hd8?EOh6wr|WlUr}*?t8Ed)TnhAa0*LgCjcI zD=flakvqhX+$sgDc=?bHhp64FBy*u|Hx=b>6-5|i?Q@6qN33GsoFBE9%K78T-g$p2 za0D50hDZj1o@}yIk=s7S%9C}hK=y4G!78r(hCF^yQ)~PGmdO(?sUDbw%25g&K-V^d zV#6AWONaz$5@IM?JX5b4R&BY1FmHD6J4{pc5bE(!IGaQ^xcc_|74E2-c!lTdJK$@c zydYoSP7Qag
  • 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%0AWMa3WQ+{p`V5c9>M_= zSX~i>g^(V?hN&&&HgF9Pc2Exx)}TCuwewBbtHSitARG$2ezq%^pi%+Ch6$QqVZsQ) zFZ?0>_a84D!g!bhgd4&GL0Cu;pfyrLB8L>RLe%B4Qm(I!N?%Q?lz6%sifBsX^kf1} z6Bv)vlV2>OV?f;XMTF!PqCiMvrCeXC6lD5@lcT7Yj^sb{(jN`5%Aorzb`7zKYM$Rd z^3tsKZ?pP)Piw1Tt$TYMhwLn3Yz5r%0AtLSNrh=soJyvt+hJ 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%0Ae@T~Cu8u9=dHSiV52M}jW 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

    YUVr#0I3 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 literal 0 HcmV?d00001 diff --git a/apps/wasmlab/bundle/icon.svg b/apps/wasmlab/bundle/icon.svg new file mode 100644 index 00000000..ca5ea759 --- /dev/null +++ b/apps/wasmlab/bundle/icon.svg @@ -0,0 +1 @@ + diff --git a/apps/wasmlab/bundle/main.splash b/apps/wasmlab/bundle/main.splash new file mode 100644 index 00000000..7b91309b --- /dev/null +++ b/apps/wasmlab/bundle/main.splash @@ -0,0 +1,257 @@ +// BEGIN shared app interface +// Shared system-app interface. Run tools/sync-app-interface.py after editing. +// Bundles carry this prelude so they work in the contained standalone host too. +// Resolve the current theme after a host restyle, not a retained prelude alias. +let theme = mod.theme +let ui_ink = theme.color_text +let ui_page = theme.color_bg_app +let ui_light = dot(ui_ink, #x36b61200) < 0.5 +let ui_surface = if ui_light { #xffffff } else { mix(ui_page, ui_ink, 0.06) } +let ui_field = mix(ui_page, ui_ink, 0.075) +let ui_hover = mix(ui_page, ui_ink, 0.12) +let ui_border = mix(ui_page, ui_ink, 0.22) +let ui_muted = mix(ui_ink, ui_page, 0.24) +let ui_link = mix(ui_ink, #x356bce, 0.25) +let ui_primary = #x285cd6 +let ui_success = mix(ui_ink, #x28a55b, 0.20) +let ui_danger = mix(ui_ink, #xe13b53, 0.20) +let UiButton = ButtonFlat{height: 44 margin: 0 padding: Inset{left: 14 right: 14} + draw_bg +: {border_radius: 12.0 border_size: 0.0 color: #x00000000 color_hover: ui_hover color_down: ui_field color_focus: ui_hover} + draw_text +: {color: ui_link color_hover: ui_link color_down: ui_link color_focus: ui_link text_style: theme.font_bold{font_size: 14}} +} +let UiPrimary = UiButton{ + draw_bg +: {color: ui_primary color_hover: #x3468df color_down: #x204cb8 color_focus: #x204cb8} + draw_text +: {color: #xffffff color_hover: #xffffff color_down: #xffffff color_focus: #xffffff} +} +let UiPill = UiButton{draw_bg +: {color: ui_surface border_radius: 22.0} draw_text +: {color: ui_ink color_hover: ui_ink color_down: ui_ink color_focus: ui_ink}} +let UiField = TextInput{width: Fill height: 48 margin: 0 padding: Inset{left: 14 right: 14 top: 12 bottom: 12} + draw_bg +: {color: ui_field color_hover: ui_field color_focus: ui_field color_empty: ui_field + border_color: ui_border border_color_hover: ui_link border_color_focus: ui_link border_color_empty: ui_border border_radius: 12.0} + draw_text +: {color: ui_ink color_hover: ui_ink color_focus: ui_ink color_empty: ui_muted color_empty_hover: ui_muted color_empty_focus: ui_muted text_style: theme.font_regular{font_size: 15}} + draw_selection.color: #x557dcc55 +} +let UiCard = RoundedView{width: Fill height: Fit flow: Down spacing: 12 padding: 20 + show_bg: true draw_bg +: {color: ui_surface border_radius: 16.0}} +let UiCaption = Label{padding: 0 flow: Flow.Right{wrap: true} width: Fill draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13 line_spacing: 1.4}}} +let UiTitle = Label{padding: 0 flow: Flow.Right{wrap: true} width: Fill draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 28 line_spacing: 1.2}}} +// END shared app interface +// Wasm Lab: Rust functions that ship in this bundle as +// fns/wasmlab.wasm (built from apps/wasmlab/guest), run by the shell's +// `wasm` service in a sandbox with a deadline and a memory cap. Each card +// calls one function through host.request and shows what came back and the +// round trip, as this script sees it. +let ground = ui_page +let ink = ui_ink +let secondary = ui_muted + +let APPS = ["Calendar" "Calculator" "Camera" "Clock" "Contacts" "Files" "Mail" "Maps" "Messages" "Music" + "News" "Notes" "Phone" "Photos" "Reminders" "Settings" "Weather" "YouTube" "Terminal" "App Hub" + "Sheets" "Wallet" "Health" "Translate" "Podcasts" "Books" "Voice Memos" "Compass" "Measure" "Stocks" + "Home" "Find My" "Shortcuts" "Magnifier" "Tips" "Fitness" "Recorder" "Gallery" "Browser" "Assistant"] +// Commas between the pairs: `] [` would read as indexing. +let BUSY = [["09:00" "09:30"], ["10:00" "11:00"], ["12:00" "13:00"], ["14:30" "15:00"], ["16:00" "17:30"]] +let OLD = "milk\neggs\nbread\nbutter\napples\ncoffee\n" +let NEW = "milk\neggs\nsourdough bread\nbutter\napples\npears\ncoffee\n" +let SAMPLE = "# Weekly notes\n\nSome *emphasis*, `code` and a [link](https://example.org).\n\n- [x] ship the MVP\n- [ ] measure on the phone\n\n| function | input |\n|---|---|\n| find_slots | busy times |\n| fuzzy_rank | names |\n\n> Quoted, with **bold** inside.\n\n" + +let ranked = [] +let rank_note = "" +let rank_seq = 0 +let duration = 30 +let slots = [] +let slots_note = "" +let diff = nil +let diff_note = "" +let md_note = "" +let md_head = "" +let rogue_note = "" +let runtime = nil +let runtime_note = "" + +// Milliseconds to one decimal. +fn ms(t0){ "" + floor((time_now() - t0) * 10000.0 + 0.5) / 10 + " ms" } + +fn rank(text){ + rank_seq = rank_seq + 1 + let seq = rank_seq + let t0 = time_now() + host.request("wasm.fuzzy_rank", {query: text items: APPS limit: 8}, fn(r){ + if seq != rank_seq { return } + if r.is_ok { + ranked = r.data.ranked + let noun = if ranked.len() == 1 { " match" } else { " matches" } + rank_note = ranked.len() + noun + " in " + ms(t0) + } + else { ranked = [] rank_note = r.error } + ui.rank_list.render() + }) +} + +fn find_slots(minutes){ + duration = minutes + let t0 = time_now() + host.request("wasm.find_slots", {day_start: "08:30" day_end: "18:00" duration: minutes busy: BUSY max: 12}, fn(r){ + if r.is_ok { slots = r.data.slots slots_note = slots.len() + " free slots of " + minutes + " min in " + ms(t0) } + else { slots = [] slots_note = r.error } + ui.durations.render() + ui.slot_list.render() + }) +} + +fn compare(){ + let t0 = time_now() + host.request("wasm.text_diff", {old: OLD new: NEW context: 1}, fn(r){ + if r.is_ok { diff = r.data diff_note = "+" + diff.added + " −" + diff.removed + " in " + ms(t0) } + else { diff = nil diff_note = r.error } + ui.diff_view.render() + }) +} + +fn render_md(copies){ + let text = "" + for i in copies { text = text + SAMPLE } + let t0 = time_now() + host.request("wasm.md_to_html", text, fn(r){ + if r.is_ok { + md_note = floor(text.len() / 100) / 10 + " KB of Markdown → " + floor(r.data.text.len() / 100) / 10 + " KB of HTML in " + ms(t0) + md_head = r.data.text.split("\n")[0] + } else { md_note = r.error md_head = "" } + ui.md_view.render() + }) +} + +fn misbehave(mode){ + rogue_note = "Running “" + mode + "”…" + ui.rogue_view.render() + let t0 = time_now() + host.request("wasm.rogue", {mode: mode}, fn(r){ + if r.is_ok { rogue_note = mode + ": returned (unexpected)" } + else { rogue_note = mode + ": " + r.error + " (after " + ms(t0) + ")" } + ui.rogue_view.render() + // The next call gets a fresh instance. + rank(ui.query.text()) + load_runtime() + }) +} + +fn load_runtime(){ + let t0 = time_now() + host.request("wasm.functions", {}, fn(r){ + if r.is_ok { runtime = r.data runtime_note = "Answered in " + ms(t0) } + else { runtime = nil runtime_note = r.error } + ui.runtime_view.render() + }) +} + +fn module_line(m){ + let how = if m.from_cache { "loaded compiled code from the cache" } else { "compiled" } + m.file + ": " + floor(m.bytes / 1024) + " KiB, " + how + " in " + m.load_ms + " ms; memory " + floor(m.memory_bytes / 1048576) + " MiB; renewed " + m.renewed + "×" +} + +// A missing key reads as `fallback` (as News and Photos do it). +fn optional(it, key, fallback){ + let value = try { it[key] } catch { nil } + if value == nil { return fallback } + value +} + +fn stats_line(name){ + let s = optional(runtime.stats, name, nil) + if s == nil { return name + ": not called yet" } + name + ": " + s.calls + " calls, mean " + s.mean_us + " µs, max " + s.max_us + " µs, " + s.errors + " errors" +} + +fn boot(){ + load_runtime() + rank("cal") + find_slots(30) + compare() + render_md(1) +} +start_timeout(0.05, || boot()) + +let Section = Label{width: Fill text: "" draw_text.color: ink draw_text.text_style: theme.font_bold{font_size: 16}} +let Note = UiCaption{text: ""} +let Line = Label{width: Fill text: "" draw_text.color: ink draw_text.text_style.font_size: 14} +let Code = Label{width: Fill text: "" flow: Flow.Right{wrap: true} draw_text.color: ink draw_text.text_style: theme.font_code{font_size: 12}} +let Choice = UiPill{} +let ChoiceOn = UiPill{ + draw_bg +: {color: ink color_hover: ink color_down: ink} + draw_text +: {color: ground color_hover: ground color_down: ground} +} + +SolidView{width: Fill height: Fill flow: Overlay align: Align{x: 0.5} show_bg: true draw_bg.color: ground + ScrollYView{width: Fill max_width: 760 height: Fill flow: Down spacing: 14 padding: Inset{left: 14 right: 14 top: 16 bottom: 32} + UiTitle{text: "Wasm Lab"} + UiCaption{text: "Rust functions shipped in this app, run by the shell in a sandbox: no files, network or other apps, a 2 s deadline and a 256 MiB memory cap."} + + UiCard{ + Section{text: "Find an app"} + query := UiField{empty_text: "Type part of a name" on_change: |text| rank(text)} + rank_list := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || { + Note{text: rank_note} + for it in ranked { Line{text: it.item + " " + it.score} } + }} + } + + UiCard{ + Section{text: "Free time today"} + UiCaption{text: "Busy 09:00–09:30, 10:00–11:00, 12:00–13:00, 14:30–15:00, 16:00–17:30; the day runs 08:30–18:00."} + durations := View{width: Fill height: Fit flow: Flow.Right{wrap: true} spacing: 8 on_render: || { + for m in [15 30 60 90] { + if m == duration { ChoiceOn{text: m + " min" on_click: || find_slots(m)} } + else { Choice{text: m + " min" on_click: || find_slots(m)} } + } + }} + slot_list := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || { + Note{text: slots_note} + for s in slots { Line{text: s[0] + " – " + s[1]} } + }} + } + + UiCard{ + Section{text: "What changed in the list"} + diff_view := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || { + Note{text: diff_note} + if diff != nil { Code{text: diff.unified} } + }} + UiButton{text: "Compare again" on_click: || compare()} + } + + UiCard{ + Section{text: "Markdown to HTML"} + md_view := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || { + Note{text: md_note} + Code{text: md_head} + }} + View{width: Fill height: Fit flow: Right spacing: 8 + UiButton{text: "Render the sample" on_click: || render_md(1)} + UiButton{text: "Render it ×40" on_click: || render_md(40)} + } + } + + UiCard{ + Section{text: "Misbehave"} + UiCaption{text: "Each ends in an error, never a crash; the next call gets a fresh instance."} + View{width: Fill height: Fit flow: Flow.Right{wrap: true} spacing: 8 + UiButton{text: "Loop forever" on_click: || misbehave("loop")} + UiButton{text: "Allocate endlessly" on_click: || misbehave("alloc")} + UiButton{text: "Panic" on_click: || misbehave("panic")} + UiButton{text: "Recurse" on_click: || misbehave("recurse")} + } + rogue_view := View{width: Fill height: Fit flow: Down on_render: || { Note{text: rogue_note} }} + } + + UiCard{ + Section{text: "Runtime"} + runtime_view := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || { + Note{text: runtime_note} + if runtime != nil { + for m in runtime.modules { Line{text: module_line(m)} } + for name in runtime.functions { Line{text: stats_line(name)} } + } + }} + UiButton{text: "Refresh" on_click: || load_runtime()} + } + } +} diff --git a/apps/wasmlab/bundle/manifest.json b/apps/wasmlab/bundle/manifest.json new file mode 100644 index 00000000..86c693de --- /dev/null +++ b/apps/wasmlab/bundle/manifest.json @@ -0,0 +1,14 @@ +{ + "schema": 1, + "id": "os.wasmlab", + "version": "0.1.0", + "name": "Wasm Lab", + "integrity": { "bundle_blake3": "" }, + "capabilities": ["wasm"], + "agent": { + "profile": "read-only", + "tools": ["ask_user_question"], + "model": { "needs": ["tool_calling"] }, + "instructions": "AGENT.md" + } +} diff --git a/apps/wasmlab/bundle/tools.json b/apps/wasmlab/bundle/tools.json new file mode 100644 index 00000000..1817b5ce --- /dev/null +++ b/apps/wasmlab/bundle/tools.json @@ -0,0 +1,199 @@ +{ + "schema": 1, + "tools": [ + { + "name": "wasmlab.find_slots", + "description": "Find free slots of `duration` minutes between day_start and day_end, around the busy intervals given, every `step` minutes. Computes only: it reads no calendar.", + "input_schema": { + "type": "object", + "properties": { + "day_start": { + "type": "string", + "description": "Start of the day, HH:MM", + "pattern": "^[0-2][0-9]:[0-5][0-9]$" + }, + "day_end": { + "type": "string", + "description": "End of the day, HH:MM", + "pattern": "^[0-2][0-9]:[0-5][0-9]$" + }, + "duration": { + "type": "integer", + "description": "Minutes each slot lasts", + "minimum": 5, + "maximum": 720 + }, + "step": { + "type": "integer", + "description": "Minutes between slot starts (default 15)", + "minimum": 5, + "maximum": 120 + }, + "busy": { + "type": "array", + "description": "Busy intervals as [start, end] pairs of HH:MM", + "maxItems": 200, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string", + "pattern": "^[0-2][0-9]:[0-5][0-9]$" + } + } + }, + "max": { + "type": "integer", + "description": "Most slots to return (default 10)", + "minimum": 1, + "maximum": 50 + } + }, + "required": [ + "day_start", + "day_end", + "duration", + "busy" + ], + "additionalProperties": false + }, + "output_schema": { + "type": "object", + "properties": { + "slots": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "required": [ + "slots" + ] + }, + "risk": "read", + "background": true, + "shareable": true, + "implemented_by": "host-service", + "host_method": "wasm.find_slots" + }, + { + "name": "wasmlab.rank", + "description": "Rank names against a query, best first: a prefix beats a word start beats a substring beats letters in order; a near miss (a typo) still ranks.", + "input_schema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "maxLength": 200 + }, + "items": { + "type": "array", + "items": { + "type": "string", + "maxLength": 500 + }, + "maxItems": 5000 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + "required": [ + "query", + "items" + ], + "additionalProperties": false + }, + "output_schema": { + "type": "object", + "properties": { + "ranked": { + "type": "array", + "items": { + "type": "object", + "properties": { + "item": { + "type": "string" + }, + "score": { + "type": "number" + } + }, + "required": [ + "item", + "score" + ] + } + } + }, + "required": [ + "ranked" + ] + }, + "risk": "read", + "background": true, + "shareable": true, + "implemented_by": "host-service", + "host_method": "wasm.fuzzy_rank" + }, + { + "name": "wasmlab.diff", + "description": "A unified line diff of two texts, with the number of lines added and removed.", + "input_schema": { + "type": "object", + "properties": { + "old": { + "type": "string", + "maxLength": 200000 + }, + "new": { + "type": "string", + "maxLength": 200000 + }, + "context": { + "type": "integer", + "description": "Unchanged lines around each change (default 3)", + "minimum": 0, + "maximum": 20 + } + }, + "required": [ + "old", + "new" + ], + "additionalProperties": false + }, + "output_schema": { + "type": "object", + "properties": { + "unified": { + "type": "string" + }, + "added": { + "type": "integer" + }, + "removed": { + "type": "integer" + } + }, + "required": [ + "unified", + "added", + "removed" + ] + }, + "risk": "read", + "background": true, + "shareable": true, + "implemented_by": "host-service", + "host_method": "wasm.text_diff" + } + ] +} diff --git a/apps/wasmlab/guest/Cargo.lock b/apps/wasmlab/guest/Cargo.lock new file mode 100644 index 00000000..cdc6fbf8 --- /dev/null +++ b/apps/wasmlab/guest/Cargo.lock @@ -0,0 +1,159 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "octosense-guest" +version = "0.1.0" +dependencies = [ + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "wasmlab-functions" +version = "0.1.0" +dependencies = [ + "octosense-guest", + "pulldown-cmark", + "serde", + "similar", + "strsim", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/wasmlab/guest/Cargo.toml b/apps/wasmlab/guest/Cargo.toml new file mode 100644 index 00000000..f71d64ce --- /dev/null +++ b/apps/wasmlab/guest/Cargo.toml @@ -0,0 +1,12 @@ +# Wasm Lab's functions, built for wasm32-unknown-unknown (./build.sh), and the +# guest SDK they use. Its own workspace: the root workspace builds natively. +[workspace] +resolver = "2" +members = ["octosense-guest", "functions"] + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/apps/wasmlab/guest/build.sh b/apps/wasmlab/guest/build.sh new file mode 100755 index 00000000..3536d074 --- /dev/null +++ b/apps/wasmlab/guest/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Builds Wasm Lab's functions into the app bundle (bundle/fns/wasmlab.wasm). +# Needs the target once: rustup target add wasm32-unknown-unknown +set -euo pipefail +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cargo build --manifest-path "$here/Cargo.toml" --release --target wasm32-unknown-unknown -p wasmlab-functions +mkdir -p "$here/../bundle/fns" +cp "$here/target/wasm32-unknown-unknown/release/wasmlab_functions.wasm" "$here/../bundle/fns/wasmlab.wasm" +echo "bundle/fns/wasmlab.wasm: $(wc -c < "$here/../bundle/fns/wasmlab.wasm") bytes" diff --git a/apps/wasmlab/guest/functions/Cargo.toml b/apps/wasmlab/guest/functions/Cargo.toml new file mode 100644 index 00000000..1ae61ce1 --- /dev/null +++ b/apps/wasmlab/guest/functions/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wasmlab-functions" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +octosense-guest = { path = "../octosense-guest" } +serde = { version = "1", features = ["derive"] } +pulldown-cmark = { version = "0.13", default-features = false, features = ["html"] } +similar = "2" +strsim = "0.11" diff --git a/apps/wasmlab/guest/functions/src/lib.rs b/apps/wasmlab/guest/functions/src/lib.rs new file mode 100644 index 00000000..3d2301aa --- /dev/null +++ b/apps/wasmlab/guest/functions/src/lib.rs @@ -0,0 +1,358 @@ +//! Wasm Lab's functions: a stand-in for a developer's own Rust crate. Light +//! computing and algorithms an app calls from its script or its agent, built +//! for wasm32 by `../build.sh` and run by the shell's runtime. + +use serde::{Deserialize, Serialize}; + +octosense_guest::abi!(); + +// ---- Markdown to HTML -------------------------------------------------------- + +/// CommonMark to HTML, with tables, task lists, strikethrough and footnotes. +pub fn md_to_html(input: &[u8]) -> Result, String> { + use pulldown_cmark::{html, Options, Parser}; + let text = std::str::from_utf8(input).map_err(|_| "the text is not UTF-8".to_string())?; + let options = Options::ENABLE_TABLES + | Options::ENABLE_TASKLISTS + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_FOOTNOTES; + let mut out = String::with_capacity(text.len() * 3 / 2); + html::push_html(&mut out, Parser::new_ext(text, options)); + Ok(out.into_bytes()) +} +octosense_guest::export!(md_to_html); + +// ---- Free meeting slots ------------------------------------------------------ + +/// A day's free slots of `duration` minutes between `day_start` and +/// `day_end`, every `step` minutes, around the `busy` intervals ("HH:MM"). +#[derive(Deserialize)] +pub struct SlotRequest { + pub day_start: String, + pub day_end: String, + pub duration: u32, + #[serde(default = "fifteen")] + pub step: u32, + #[serde(default)] + pub busy: Vec<[String; 2]>, + #[serde(default = "ten")] + pub max: usize, +} + +#[derive(Serialize, Debug, PartialEq)] +pub struct Slots { + pub slots: Vec<[String; 2]>, +} + +fn fifteen() -> u32 { + 15 +} + +fn ten() -> usize { + 10 +} + +fn minutes(hhmm: &str) -> Result { + let (h, m) = hhmm + .split_once(':') + .ok_or_else(|| format!("{hhmm} is not HH:MM"))?; + let (h, m): (u32, u32) = ( + h.parse().map_err(|_| format!("{hhmm} is not HH:MM"))?, + m.parse().map_err(|_| format!("{hhmm} is not HH:MM"))?, + ); + if h > 24 || m > 59 || (h == 24 && m > 0) { + return Err(format!("{hhmm} is not a time of day")); + } + Ok(h * 60 + m) +} + +fn hhmm(minutes: u32) -> String { + format!("{:02}:{:02}", minutes / 60, minutes % 60) +} + +pub fn find_slots(req: SlotRequest) -> Result { + let (start, end) = (minutes(&req.day_start)?, minutes(&req.day_end)?); + if start >= end { + return Err("the day ends before it starts".into()); + } + if req.duration == 0 || req.step == 0 { + return Err("the duration and the step must be at least a minute".into()); + } + let mut busy = Vec::with_capacity(req.busy.len()); + for [from, to] in &req.busy { + let (from, to) = (minutes(from)?, minutes(to)?); + if from < to { + busy.push((from.max(start), to.min(end))); + } + } + busy.sort_unstable(); + // Merge overlapping and touching busy intervals. + let mut merged: Vec<(u32, u32)> = Vec::with_capacity(busy.len()); + for (from, to) in busy { + match merged.last_mut() { + Some(last) if from <= last.1 => last.1 = last.1.max(to), + _ => merged.push((from, to)), + } + } + let mut slots = Vec::new(); + let mut free_from = start; + for (from, to) in merged.into_iter().chain([(end, end)]) { + // Candidate starts sit on the step grid from the start of the day. + let mut at = start + (free_from - start).div_ceil(req.step) * req.step; + while at + req.duration <= from && slots.len() < req.max { + slots.push([hhmm(at), hhmm(at + req.duration)]); + at += req.step; + } + free_from = free_from.max(to); + } + Ok(Slots { slots }) +} +octosense_guest::export_json!(find_slots); + +// ---- Fuzzy ranking ----------------------------------------------------------- + +/// The items that match `query`, best first: a prefix beats a substring, a +/// substring beats letters in order, and a near miss still ranks (typos). +#[derive(Deserialize)] +pub struct RankRequest { + pub query: String, + pub items: Vec, + #[serde(default = "ten")] + pub limit: usize, +} + +#[derive(Serialize, Debug, PartialEq)] +pub struct Ranked { + pub item: String, + pub score: f64, +} + +/// An object, not a bare list, so an agent tool can return it. +#[derive(Serialize, Debug, PartialEq)] +pub struct Ranking { + pub ranked: Vec, +} + +fn score(query: &str, item: &str) -> f64 { + let (q, it) = (query.to_lowercase(), item.to_lowercase()); + if q.is_empty() { + return 0.0; + } + if it.starts_with(&q) { + return 1.0 - (it.chars().count() - q.chars().count()) as f64 / 200.0; + } + if let Some(at) = it.find(&q) { + // A match at a word start is nearly as good as a prefix. + let word_start = it[..at].ends_with([' ', '-', '_', '.', '/']); + return if word_start { 0.95 } else { 0.85 } - at as f64 / 200.0; + } + // Letters in order: tighter spans score higher. + let mut chars = it.char_indices(); + let mut first = None; + let mut last = 0; + let mut in_order = true; + for c in q.chars() { + match chars.find(|(_, x)| *x == c) { + Some((i, _)) => { + first.get_or_insert(i); + last = i; + } + None => { + in_order = false; + break; + } + } + } + if in_order { + let span = (last - first.unwrap_or(0) + 1) as f64; + return 0.5 + 0.3 * (q.len() as f64 / span).min(1.0); + } + strsim::jaro_winkler(&q, &it) * 0.6 +} + +pub fn fuzzy_rank(req: RankRequest) -> Result { + let mut ranked: Vec = req + .items + .iter() + .map(|item| Ranked { + score: (score(&req.query, item) * 1000.0).round() / 1000.0, + item: item.clone(), + }) + .filter(|r| r.score >= 0.45) + .collect(); + ranked.sort_by(|a, b| { + b.score + .total_cmp(&a.score) + .then_with(|| a.item.cmp(&b.item)) + }); + ranked.truncate(req.limit); + Ok(Ranking { ranked }) +} +octosense_guest::export_json!(fuzzy_rank); + +// ---- Line diff --------------------------------------------------------------- + +/// A unified diff of two texts, line by line, with `context` lines around +/// each change. +#[derive(Deserialize)] +pub struct DiffRequest { + pub old: String, + pub new: String, + #[serde(default = "three")] + pub context: usize, +} + +#[derive(Serialize, Debug, PartialEq)] +pub struct Diff { + pub unified: String, + pub added: usize, + pub removed: usize, +} + +fn three() -> usize { + 3 +} + +pub fn text_diff(req: DiffRequest) -> Result { + use similar::{ChangeTag, TextDiff}; + let diff = TextDiff::from_lines(&req.old, &req.new); + let (mut added, mut removed) = (0, 0); + for change in diff.iter_all_changes() { + match change.tag() { + ChangeTag::Insert => added += 1, + ChangeTag::Delete => removed += 1, + ChangeTag::Equal => {} + } + } + let unified = diff + .unified_diff() + .context_radius(req.context) + .header("old", "new") + .to_string(); + Ok(Diff { + unified, + added, + removed, + }) +} +octosense_guest::export_json!(text_diff); + +// ---- Misbehaviour, for the containment checks -------------------------------- + +#[derive(Deserialize)] +pub struct RogueRequest { + pub mode: String, +} + +/// Misbehaves on purpose: `loop` never returns, `alloc` takes memory until +/// it cannot, `panic` panics, `recurse` overflows the stack. +pub fn rogue(req: RogueRequest) -> Result { + match req.mode.as_str() { + "loop" => { + let mut n = 0u64; + loop { + n = std::hint::black_box(n.wrapping_add(1)); + } + } + "alloc" => { + let mut hoard: Vec> = Vec::new(); + loop { + hoard.push(vec![1; 16 << 20]); + std::hint::black_box(&hoard); + } + } + "panic" => panic!("rogue: a deliberate panic"), + "recurse" => Ok(depth(0).to_string()), + other => Err(format!("rogue has no mode {other}")), + } +} +octosense_guest::export_json!(rogue); + +#[allow(unconditional_recursion)] +fn depth(n: u64) -> u64 { + std::hint::black_box(depth(std::hint::black_box(n + 1))) + 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn slots(busy: &[[&str; 2]], duration: u32) -> Vec<[String; 2]> { + let req = SlotRequest { + day_start: "09:00".into(), + day_end: "12:00".into(), + duration, + step: 30, + busy: busy + .iter() + .map(|[a, b]| [a.to_string(), b.to_string()]) + .collect(), + max: 10, + }; + find_slots(req).unwrap().slots + } + + #[test] + fn free_slots_go_around_busy_time_on_the_step_grid() { + let found = slots( + &[["09:30", "10:15"], ["10:00", "10:40"], ["11:30", "12:30"]], + 30, + ); + let found: Vec = found.iter().map(|[a, b]| format!("{a}-{b}")).collect(); + assert_eq!(found, ["09:00-09:30", "11:00-11:30"]); + assert_eq!(slots(&[], 60).len(), 5); + } + + #[test] + fn bad_times_are_refused() { + let req = SlotRequest { + day_start: "9am".into(), + day_end: "12:00".into(), + duration: 30, + step: 15, + busy: vec![], + max: 10, + }; + assert!(find_slots(req).is_err()); + } + + #[test] + fn a_prefix_beats_a_substring_beats_letters_in_order() { + let items = ["Calculator", "Calendar", "Mail", "Local calls", "Clock"] + .map(String::from) + .to_vec(); + let ranked = fuzzy_rank(RankRequest { + query: "cal".into(), + items, + limit: 10, + }) + .unwrap() + .ranked; + let names: Vec<&str> = ranked.iter().map(|r| r.item.as_str()).collect(); + assert_eq!(&names[..2], ["Calendar", "Calculator"]); + assert!(names.contains(&"Local calls")); + assert!(!names.contains(&"Mail")); + } + + #[test] + fn a_diff_counts_its_lines() { + let diff = text_diff(DiffRequest { + old: "a\nb\nc\n".into(), + new: "a\nB\nc\nd\n".into(), + context: 1, + }) + .unwrap(); + assert_eq!((diff.added, diff.removed), (2, 1)); + assert!(diff.unified.contains("-b\n+B\n")); + } + + #[test] + fn markdown_becomes_html() { + let html = + String::from_utf8(md_to_html(b"# Hi\n\n- [x] done\n\n| a |\n|---|\n| 1 |\n").unwrap()) + .unwrap(); + assert!( + html.contains("

    Hi

    ") && html.contains("checkbox") && html.contains("") + ); + } +} diff --git a/apps/wasmlab/guest/octosense-guest/Cargo.toml b/apps/wasmlab/guest/octosense-guest/Cargo.toml new file mode 100644 index 00000000..6ea63a4f --- /dev/null +++ b/apps/wasmlab/guest/octosense-guest/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "octosense-guest" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[dependencies] +serde_json = "1" diff --git a/apps/wasmlab/guest/octosense-guest/src/lib.rs b/apps/wasmlab/guest/octosense-guest/src/lib.rs new file mode 100644 index 00000000..b97c86e5 --- /dev/null +++ b/apps/wasmlab/guest/octosense-guest/src/lib.rs @@ -0,0 +1,164 @@ +//! The guest side of OctoSense's WebAssembly functions: what a Rust crate +//! needs so an app can call its functions through the shell's runtime +//! (`crates/wasm-host`, whose docs define the ABI). +//! +//! ```ignore +//! octosense_guest::abi!(); +//! +//! fn shout(input: &[u8]) -> Result, String> { +//! Ok(input.to_ascii_uppercase()) +//! } +//! octosense_guest::export!(shout); +//! +//! #[derive(serde::Deserialize)] +//! struct Order { items: Vec } +//! fn total(order: Order) -> Result { +//! Ok(order.items.iter().sum()) +//! } +//! octosense_guest::export_json!(total); +//! ``` +//! +//! Build the crate as a `cdylib` for `wasm32-unknown-unknown`. The module +//! exports `memory`, `octo_alloc`, `octo_free` and each exported function, +//! and imports only `octo.log`. + +use std::alloc::{alloc, dealloc, Layout}; + +#[doc(hidden)] +pub use serde_json; + +#[cfg(target_arch = "wasm32")] +#[link(wasm_import_module = "octo")] +extern "C" { + #[link_name = "log"] + fn octo_log(ptr: *const u8, len: usize); +} + +/// Writes a line to the app's log. The host keeps a bounded number of +/// lines, each cut to a bounded length. +pub fn log(line: &str) { + #[cfg(target_arch = "wasm32")] + unsafe { + octo_log(line.as_ptr(), line.len()) + } + #[cfg(not(target_arch = "wasm32"))] + eprintln!("{line}"); +} + +/// Exports `octo_alloc` and `octo_free`, which the host uses to pass input +/// in and take output out. Invoke it once in the crate. +#[macro_export] +macro_rules! abi { + () => { + const _: () = { + #[export_name = "octo_alloc"] + pub extern "C" fn __octo_alloc(len: usize) -> *mut u8 { + $crate::__alloc(len) + } + + #[export_name = "octo_free"] + pub unsafe extern "C" fn __octo_free(ptr: *mut u8, len: usize) { + $crate::__free(ptr, len) + } + }; + }; +} + +/// Exports `fn(&[u8]) -> Result, String>` under its own name. +#[macro_export] +macro_rules! export { + ($name:ident) => { + const _: () = { + #[export_name = stringify!($name)] + pub extern "C" fn __octo_export(ptr: *const u8, len: usize) -> i64 { + $crate::__init(); + let input = unsafe { $crate::__input(ptr, len) }; + $crate::__output($name(input)) + } + }; + }; +} + +/// Exports `fn(In) -> Result` under its own name, where `In` +/// is deserialized from the input as JSON and `Out` serialized to it. +#[macro_export] +macro_rules! export_json { + ($name:ident) => { + const _: () = { + #[export_name = stringify!($name)] + pub extern "C" fn __octo_export(ptr: *const u8, len: usize) -> i64 { + $crate::__init(); + let input = unsafe { $crate::__input(ptr, len) }; + $crate::__output( + $crate::serde_json::from_slice(input) + .map_err(|e| { + ::std::format!("the input is not what {} takes: {e}", stringify!($name)) + }) + .and_then($name) + .and_then(|out| { + $crate::serde_json::to_vec(&out).map_err(|e| e.to_string()) + }), + ) + } + }; + }; +} + +#[doc(hidden)] +pub fn __alloc(len: usize) -> *mut u8 { + if len == 0 { + return std::ptr::NonNull::dangling().as_ptr(); + } + // SAFETY: the size is non-zero and the alignment is 1. + unsafe { alloc(Layout::from_size_align_unchecked(len, 1)) } +} + +/// # Safety +/// `ptr` and `len` are a buffer from [`__alloc`] or [`__output`]. +#[doc(hidden)] +pub unsafe fn __free(ptr: *mut u8, len: usize) { + if len != 0 { + dealloc(ptr, Layout::from_size_align_unchecked(len, 1)); + } +} + +/// # Safety +/// `ptr` and `len` are the input buffer the host wrote. +#[doc(hidden)] +pub unsafe fn __input<'a>(ptr: *const u8, len: usize) -> &'a [u8] { + if len == 0 { + &[] + } else { + std::slice::from_raw_parts(ptr, len) + } +} + +/// Packs a result for the host: a status byte (`0` ok, `1` error) and the +/// output or the error text, as `(ptr << 32) | len`. +#[doc(hidden)] +pub fn __output(result: Result, String>) -> i64 { + let mut buf = Vec::new(); + match result { + Ok(bytes) => { + buf.reserve_exact(bytes.len() + 1); + buf.push(0); + buf.extend_from_slice(&bytes); + } + Err(text) => { + buf.reserve_exact(text.len() + 1); + buf.push(1); + buf.extend_from_slice(text.as_bytes()); + } + } + let boxed = buf.into_boxed_slice(); + let len = boxed.len(); + let ptr = Box::into_raw(boxed) as *mut u8 as usize; + ((ptr as u64) << 32 | len as u64) as i64 +} + +/// Logs a panic's message before the module traps, so the app can tell why. +#[doc(hidden)] +pub fn __init() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))))); +} diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index de35baf7..8284f602 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -72,6 +72,8 @@ octosense-ai-host = { workspace = true } octosense-mail-service = { workspace = true, optional = true } octosense-oauth-service = { workspace = true, optional = true, features = ["host"] } octosense-markdown-editor = { workspace = true, optional = true } +# Apps' own WebAssembly functions (the `wasm` service, ADR 0011). +octosense-wasm-host = { workspace = true, optional = true } octosense-calendar-service = { workspace = true, optional = true } octosense-news-service = { workspace = true, optional = true } # App Hub's host-service registry: the `glance` service registers into it. @@ -159,6 +161,9 @@ app-aichat = ["dep:makepad-aichat"] # development builds without it (build.rs); a release build without it # honours only the `--dev-grant-all` launch flag. dev-mode = [] +# The `wasm` host service (ADR 0011), which runs an app's own +# WebAssembly functions (crates/wasm-host). Off by default. +wasm-lab = ["app-hub", "dep:octosense-wasm-host"] # The system toolbox offered to app agents as host-routed tools (ADR 0002 # §6, ADR 0004 §12; src/host_tools/toolbox.rs): the `toolbox` owner in the # host-tool relay. Off by default. diff --git a/crates/shell/src/apps.rs b/crates/shell/src/apps.rs index 750fa38f..52205359 100644 --- a/crates/shell/src/apps.rs +++ b/crates/shell/src/apps.rs @@ -252,6 +252,9 @@ pub fn register_host_services() { // published the same way. register_calendar_services(); register_news(); + // Apps' own WebAssembly functions (ADR 0011). + #[cfg(feature = "wasm-lab")] + crate::wasm_service::register(); // After every service of the shell's own: the notice service never // stands in for one. let served = crate::glance_notice::serve_system_apps(); diff --git a/crates/shell/src/host_tools/script_apps.rs b/crates/shell/src/host_tools/script_apps.rs index 915663c5..9c7edaf8 100644 --- a/crates/shell/src/host_tools/script_apps.rs +++ b/crates/shell/src/host_tools/script_apps.rs @@ -192,7 +192,7 @@ pub fn load(app: &str) -> Result<(), String> { /// `app`'s admitted bundle: a system app's packed bundle, or an installed /// one, with App Hub's apps root. -fn admitted_bundle(app: &str) -> Result<(PathBuf, PathBuf), String> { +pub(crate) fn admitted_bundle(app: &str) -> Result<(PathBuf, PathBuf), String> { // Never a native app's tools, executor or grants (ADR 0004 §3, §7). crate::apps::check_script_app_id(app)?; let root = octosense_appstore::data_root_if_set().ok_or("App Hub has no apps root yet")?; diff --git a/crates/shell/src/lib.rs b/crates/shell/src/lib.rs index 2381e546..3b39c225 100644 --- a/crates/shell/src/lib.rs +++ b/crates/shell/src/lib.rs @@ -100,6 +100,8 @@ pub mod connected_backends; pub mod glance_digest; #[cfg(any(feature = "app-hub", native_mobile))] pub mod glance_notice; +#[cfg(feature = "wasm-lab")] +pub mod wasm_service; pub mod glance_panel; pub mod glance_sheet; mod card_presentation; diff --git a/crates/shell/src/wasm_service.rs b/crates/shell/src/wasm_service.rs new file mode 100644 index 00000000..3b2952b9 --- /dev/null +++ b/crates/shell/src/wasm_service.rs @@ -0,0 +1,425 @@ +//! The `wasm` service (ADR 0011, feature `wasm-lab`): an app's own +//! functions, written in Rust and shipped in its bundle as `fns/*.wasm`, +//! run for that app by `octosense-wasm-host` (Wasmtime, compiled by +//! Cranelift, with a deadline and a memory cap). They are for light +//! computing and algorithms beside the app's script: parsing, ranking, +//! scheduling, diffing. Splash's own compute handles kernels. +//! +//! - `wasm.functions` answers what the app's modules export, how they +//! loaded and how each function has run so far. +//! - `wasm.` calls one. Its input is the request's arguments: a +//! string as its text, anything else as JSON. Its output comes back as +//! JSON, or as `{"text": …}` when it is not JSON. Its own error, a trap or +//! its deadline is the request's error. +//! +//! **Whose code.** A module comes only from the calling app's own admitted +//! bundle (digest-checked, [`script_apps::admitted_bundle`]): no argument +//! names a file, and no app reaches another's functions. The Card runner's +//! gate and the tool executor both require the app's `wasm` capability, and +//! the service checks the admitted manifest again before it loads anything. +//! +//! **Where it runs.** Requests arrive on the UI thread. Each app gets one +//! worker thread holding its instances, so a slow function holds up only +//! its own app. Compiled code is cached in the host directory +//! (`wasm-cache`), keyed by the module's digest. An instance that trapped or +//! ran past its deadline is replaced before the next call. +//! +//! Not done yet: an app updated while the shell runs keeps its old +//! functions until the shell restarts, and a worker lives as long as the +//! shell. + +use std::collections::{BTreeMap, HashMap}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{channel, Receiver, SendError, Sender}; +use std::sync::{Mutex, OnceLock}; +use std::time::Instant; + +use octosense_appstore::services::{HostService, Replier, ServiceCall, ServiceHost}; +use octosense_wasm_host::{Instance, Limits, Program, Runtime}; +use serde_json::{json, Value}; + +use crate::host_tools::script_apps; + +/// The most modules one app's bundle may carry. +const MAX_MODULES: usize = 8; + +/// One engine, one epoch ticker and one code cache for the process. +static RUNTIME: OnceLock> = OnceLock::new(); + +/// Each app's worker, by app id. +static WORKERS: Mutex>>> = Mutex::new(None); + +struct Job { + method: String, + args: Value, + host_dir: PathBuf, + reply: Replier, +} + +/// The `wasm` family for the Card runner and the tool executor. +pub struct WasmService; + +impl HostService for WasmService { + fn family(&self) -> &'static str { + "wasm" + } + + fn call(&mut self, call: ServiceCall, reply: Replier, _host: &mut dyn ServiceHost) { + if call.from_sheet { + reply.send(Err("the wasm service has no sheet".into())); + return; + } + let app = call.app_id.clone(); + let job = Job { method: call.method().to_string(), host_dir: call.host_dir, args: call.args, reply }; + let mut workers = WORKERS.lock().unwrap_or_else(|e| e.into_inner()); + let workers = workers.get_or_insert_with(HashMap::new); + // A worker that ended (only a panic ends one) is replaced. + let job = match workers.get(&app) { + Some(jobs) => match jobs.send(job) { + Ok(()) => return, + Err(SendError(job)) => job, + }, + None => job, + }; + let (jobs, queue) = channel(); + let name = app.clone(); + let spawned = std::thread::Builder::new().name(format!("wasm {app}")).spawn(move || work(name, queue)); + if let Err(error) = spawned { + job.reply.send(Err(format!("cannot start {app}'s functions: {error}"))); + return; + } + let _ = jobs.send(job); + workers.insert(app, jobs); + } +} + +pub fn register() { + octosense_appstore::services::register_host_service(Box::new(WasmService)); +} + +/// One app's worker: loads its modules on the first request (again on the +/// next one, if that failed), then answers each request in turn. +fn work(app: String, jobs: Receiver) { + let mut lab: Option = None; + for job in jobs { + if lab.is_none() { + match Lab::load(&app, &job.host_dir) { + Ok(loaded) => lab = Some(loaded), + Err(error) => { + makepad_widgets::log!("wasm {app}: {error}"); + job.reply.send(Err(error)); + continue; + } + } + } + if let Some(lab) = lab.as_mut() { + job.reply.send(lab.answer(&job.method, &job.args)); + } + } +} + +fn runtime(host_dir: &Path) -> Result<&'static Runtime, String> { + RUNTIME + .get_or_init(|| Runtime::new(Limits::default(), Some(host_dir.join("wasm-cache")))) + .as_ref() + .map_err(Clone::clone) +} + +/// One app's loaded modules. +struct Lab { + app: String, + runtime: &'static Runtime, + modules: Vec, + /// Which module exports each function. + owner: BTreeMap, + stats: BTreeMap, +} + +struct Module { + file: String, + bytes: usize, + load_ms: f64, + from_cache: bool, + program: Program, + instance: Instance, + /// Fresh instances made after a trap or a deadline. + renewed: u32, +} + +/// How one function has run: in the worker, from input to output. +#[derive(Default)] +struct Stats { + calls: u64, + errors: u64, + total_us: f64, + max_us: f64, +} + +impl Lab { + fn load(app: &str, host_dir: &Path) -> Result { + let (_, bundle) = script_apps::admitted_bundle(app)?; + if !script_apps::from_bundle(&bundle)?.families.contains("wasm") { + return Err(format!("{app} was not granted the wasm service")); + } + let runtime = runtime(host_dir)?; + let mut files: Vec = std::fs::read_dir(bundle.join("fns")) + .map_err(|_| format!("{app}'s bundle has no fns directory"))? + .flatten() + .map(|entry| entry.path()) + .filter(|path| { + path.extension().is_some_and(|ext| ext == "wasm") + && std::fs::symlink_metadata(path).is_ok_and(|m| m.is_file()) + }) + .collect(); + files.sort(); + if files.is_empty() || files.len() > MAX_MODULES { + return Err(format!("{app}'s bundle must carry 1 to {MAX_MODULES} fns/*.wasm modules")); + } + let mut lab = Lab { app: app.to_string(), runtime, modules: Vec::new(), owner: BTreeMap::new(), stats: BTreeMap::new() }; + for path in files { + let file = path.file_name().unwrap_or_default().to_string_lossy().into_owned(); + let bytes = std::fs::read(&path).map_err(|e| format!("{file}: {e}"))?; + let started = Instant::now(); + let program = runtime.load(&bytes).map_err(|e| format!("{file}: {e}"))?; + let instance = runtime.instantiate(&program).map_err(|e| format!("{file}: {e}"))?; + let load_ms = started.elapsed().as_secs_f64() * 1e3; + for function in program.functions() { + if lab.owner.insert(function.clone(), lab.modules.len()).is_some() { + return Err(format!("{file}: {function} is exported by another module too")); + } + } + let from_cache = program.from_cache(); + makepad_widgets::log!( + "wasm {app}: {file} ({} KiB) {} in {load_ms:.1} ms: {}", + bytes.len() / 1024, + if from_cache { "loaded from the cache" } else { "compiled" }, + program.functions().join(", ") + ); + lab.modules.push(Module { file, bytes: bytes.len(), load_ms, from_cache, program, instance, renewed: 0 }); + } + Ok(lab) + } + + fn answer(&mut self, method: &str, args: &Value) -> Result { + if method == "functions" { + // Also in the log: where there is no remote instrument (an + // Android build), the numbers are read from logcat. + let described = self.describe(); + makepad_widgets::log!("wasm {}: {described}", self.app); + return Ok(described); + } + let Some(&index) = self.owner.get(method) else { + return Err(format!("{} has no function {method:?}", self.app)); + }; + let module = &mut self.modules[index]; + if module.instance.spent() { + module.instance = self.runtime.instantiate(&module.program).map_err(|e| e.to_string())?; + module.renewed += 1; + } + let input = match args { + Value::String(text) => text.clone().into_bytes(), + other => serde_json::to_vec(other).map_err(|e| e.to_string())?, + }; + let started = Instant::now(); + let result = module.instance.call(method, &input); + let us = started.elapsed().as_secs_f64() * 1e6; + for line in module.instance.take_logs() { + makepad_widgets::log!("wasm {}: {line}", self.app); + } + let stats = self.stats.entry(method.to_string()).or_default(); + stats.calls += 1; + stats.total_us += us; + stats.max_us = stats.max_us.max(us); + let output = result.map_err(|error| { + stats.errors += 1; + error.to_string() + })?; + Ok(serde_json::from_slice(&output).unwrap_or_else(|_| json!({"text": String::from_utf8_lossy(&output)}))) + } + + fn describe(&self) -> Value { + let modules: Vec = self + .modules + .iter() + .map(|m| { + json!({"file": m.file, "bytes": m.bytes, "load_ms": round(m.load_ms), + "from_cache": m.from_cache, "memory_bytes": m.instance.memory_bytes(), "renewed": m.renewed}) + }) + .collect(); + let stats: serde_json::Map = self + .stats + .iter() + .map(|(name, s)| { + let mean = if s.calls == 0 { 0.0 } else { s.total_us / s.calls as f64 }; + (name.clone(), json!({"calls": s.calls, "errors": s.errors, "mean_us": round(mean), "max_us": round(s.max_us)})) + }) + .collect(); + json!({"functions": self.owner.keys().collect::>(), "modules": modules, "stats": stats}) + } +} + +fn round(value: f64) -> f64 { + (value * 10.0).round() / 10.0 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ai_host::app_peers::host_tools::{HostToolCall, ToolExecutor, ToolReply}; + use crate::host_tools::script_apps::{self, HostServiceExecutor}; + use std::sync::Arc; + use std::time::Duration; + + /// Wasm Lab's agent gets exactly its three function tools, each routed + /// to the app's own function on the `wasm` service. + #[test] + fn wasm_lab_offers_its_functions_as_tools() { + let dir = script_apps::tests::stamped_bundle("wasmlab", "tools", |_, _| {}); + let loaded = script_apps::from_bundle(&dir).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + let names: Vec<&str> = loaded.tools.iter().filter_map(|t| t["name"].as_str()).collect(); + assert_eq!(names, ["wasmlab.find_slots", "wasmlab.rank", "wasmlab.diff"]); + assert_eq!(loaded.host_methods["wasmlab.find_slots"], "wasm.find_slots"); + assert_eq!(loaded.host_methods["wasmlab.rank"], "wasm.fuzzy_rank"); + assert_eq!(loaded.host_methods["wasmlab.diff"], "wasm.text_diff"); + assert!(loaded.tools.iter().all(|t| t["risk"] == "read" && t["output_schema"]["type"] == "object")); + assert_eq!(loaded.families.iter().collect::>(), ["wasm"]); + assert_eq!(loaded.generic, ["ask_user_question"]); + } + + struct NoSheet; + impl ServiceHost for NoSheet { + fn open_sheet(&mut self, _body: String) {} + fn close_sheet(&mut self) {} + } + + static NEXT_HEAP: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1 << 40); + + /// What the app's script would get from `host.request(service, args)`. + fn request(app: &str, service: &str, args: Value, host_dir: &Path) -> Result { + let heap = NEXT_HEAP.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let call = ServiceCall { app_id: app.into(), service: service.into(), args, from_sheet: false, may_prompt: true, host_dir: host_dir.into() }; + octosense_appstore::services::dispatch(call, heap, 1, &mut NoSheet); + for _ in 0..1000 { + if let Some((_, _, answer)) = octosense_appstore::services::take_replies_for(&[heap]).pop() { + return answer.map(|text| serde_json::from_str(&text).unwrap()); + } + std::thread::sleep(Duration::from_millis(10)); + } + panic!("{service} was never answered"); + } + + /// Register a stamped copy of `apps/wasmlab/bundle` as the system app + /// `id`, as a build that selects it does. + fn ship(id: &'static str, tag: &str, edit: impl FnOnce(&Path, &mut Value)) { + let dir = script_apps::tests::stamped_bundle("wasmlab", tag, |dir, manifest| { + manifest["id"] = json!(id); + edit(dir, manifest); + }); + let packed = octosense_app_hub::pack::pack_system_app(&dir).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + octosense_appstore::system::register_system_app(octosense_appstore::system::SystemApp { + id, + name: "Wasm Lab", + pack: Box::leak(packed.pack_json.into_boxed_str()), + assets: &[], + }); + } + + /// The whole path, in a process of its own (the apps root and the + /// registries are process-wide): Wasm Lab's tools reach its functions + /// through the executor and the service, its script's requests do too, + /// rogue code ends in an error and a fresh instance, and an app without + /// the grant reaches nothing. + #[test] + fn the_apps_own_functions_answer_its_tools_and_its_script() { + const CHILD: &str = "OCTOSENSE_TEST_WASM_SERVICE"; + if std::env::var_os(CHILD).is_none() { + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "wasm_service::tests::the_apps_own_functions_answer_its_tools_and_its_script", "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + assert!(output.status.success(), "{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); + return; + } + let root = std::env::temp_dir().join(format!("octosense-wasm-service-{}", std::process::id())); + let host_dir = root.join(".host"); + octosense_appstore::set_data_root(root.clone()); + ship("os.wasmlab", "service", |_, _| {}); + // The same bundle without the capability (and so without its tools). + ship("os.wasmplain", "plain", |dir, manifest| { + manifest["capabilities"] = json!([]); + manifest.as_object_mut().unwrap().remove("agent"); + std::fs::remove_file(dir.join("tools.json")).unwrap(); + std::fs::remove_file(dir.join("AGENT.md")).unwrap(); + }); + register(); + + // An agent's tool call, as the relay hands it to the app's executor. + let (_, bundle) = script_apps::admitted_bundle("os.wasmlab").unwrap(); + let loaded = script_apps::from_bundle(&bundle).unwrap(); + let executor = HostServiceExecutor { + app: "os.wasmlab".into(), + tools: loaded.host_service_tools, + methods: loaded.host_methods, + families: loaded.families, + host_dir: host_dir.clone(), + }; + let sent: Arc>> = Arc::default(); + let into = sent.clone(); + let call = HostToolCall::parse(&json!({"peer": "p", "session_id": "s", "turn_id": "t", "call_id": "c1", + "name": "wasmlab.find_slots", + "args": {"day_start": "09:00", "day_end": "12:00", "duration": 30, "busy": [["09:30", "11:00"]]}})) + .unwrap(); + executor.execute(call, ToolReply::new("c1", move |v| into.lock().unwrap().push(v))); + for _ in 0..1000 { + script_apps::poll(); + if !sent.lock().unwrap().is_empty() { + break; + } + std::thread::sleep(Duration::from_millis(10)); + } + let answer = sent.lock().unwrap().pop().expect("the tool answered"); + assert_eq!(answer["ok"], true, "{answer}"); + // Every 15 minutes, the default step. + assert_eq!(answer["data"]["slots"], json!([["09:00", "09:30"], ["11:00", "11:30"], ["11:15", "11:45"], ["11:30", "12:00"]]), "{answer}"); + + // The script's own requests: a string goes in as its text, and an + // output that is not JSON comes back as text. + let html = request("os.wasmlab", "wasm.md_to_html", json!("# Hi"), &host_dir).unwrap(); + assert_eq!(html["text"], "

    Hi

    \n"); + let ranked = request("os.wasmlab", "wasm.fuzzy_rank", json!({"query": "cal", "items": ["Mail", "Calendar"]}), &host_dir).unwrap(); + assert_eq!(ranked["ranked"][0]["item"], "Calendar"); + let error = request("os.wasmlab", "wasm.find_slots", json!({"day_start": "12:00", "day_end": "09:00", "duration": 30, "busy": []}), &host_dir).unwrap_err(); + assert!(error.contains("ends before it starts"), "{error}"); + let error = request("os.wasmlab", "wasm.nothing", json!({}), &host_dir).unwrap_err(); + assert!(error.contains("no function"), "{error}"); + + // Rogue code: an error each time, and the next call still answers. + for mode in ["loop", "alloc", "panic", "recurse"] { + let error = request("os.wasmlab", "wasm.rogue", json!({"mode": mode}), &host_dir).unwrap_err(); + assert!(error.contains("deadline") || error.contains("trapped"), "{mode}: {error}"); + let ranked = request("os.wasmlab", "wasm.fuzzy_rank", json!({"query": "m", "items": ["Mail"]}), &host_dir).unwrap(); + assert_eq!(ranked["ranked"][0]["item"], "Mail", "{mode}"); + } + let described = request("os.wasmlab", "wasm.functions", json!({}), &host_dir).unwrap(); + assert_eq!(described["functions"], json!(["find_slots", "fuzzy_rank", "md_to_html", "rogue", "text_diff"])); + assert_eq!(described["modules"][0]["file"], "wasmlab.wasm"); + assert_eq!(described["modules"][0]["renewed"], 4); + assert_eq!(described["stats"]["rogue"]["errors"], 4); + assert!(host_dir.join("wasm-cache").read_dir().unwrap().next().is_some(), "compiled code is cached"); + + // No grant, no functions: the service checks the admitted manifest + // itself, whoever dispatched the request. + let error = request("os.wasmplain", "wasm.fuzzy_rank", json!({"query": "m", "items": []}), &host_dir).unwrap_err(); + assert!(error.contains("was not granted the wasm service"), "{error}"); + // And a sheet never reaches it. + let heap = NEXT_HEAP.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let call = ServiceCall { app_id: "os.wasmlab".into(), service: "wasm.functions".into(), args: json!({}), from_sheet: true, may_prompt: true, host_dir: host_dir.clone() }; + octosense_appstore::services::dispatch(call, heap, 1, &mut NoSheet); + let (_, _, answer) = octosense_appstore::services::take_replies_for(&[heap]).pop().unwrap(); + assert!(answer.unwrap_err().contains("no sheet")); + let _ = std::fs::remove_dir_all(root); + } +} diff --git a/crates/wasm-host/Cargo.toml b/crates/wasm-host/Cargo.toml new file mode 100644 index 00000000..3117e85b --- /dev/null +++ b/crates/wasm-host/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "octosense-wasm-host" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +publish = false + +[dependencies] +wasmtime = { workspace = true } +sha2 = "0.10" +# Not used directly: Wasmtime reads a trap's registers through libc's +# ucontext_t, and before 0.2.190 Android aarch64's lacked the kernel's +# padding after the signal mask, so the faulting pc was misread, no trap was +# recognised as wasm and every trap ended the app. +libc = "0.2.190" + +[dev-dependencies] +wat = "1" +serde_json = { workspace = true } +# The sample functions natively, to check the module against them. +wasmlab-functions = { path = "../../apps/wasmlab/guest/functions" } diff --git a/crates/wasm-host/examples/measure.rs b/crates/wasm-host/examples/measure.rs new file mode 100644 index 00000000..2813830c --- /dev/null +++ b/crates/wasm-host/examples/measure.rs @@ -0,0 +1,113 @@ +//! Startup and per-call latency of Wasm Lab's functions in the runtime, +//! against the same functions called natively. +//! +//! cargo run --release -p octosense-wasm-host --example measure + +use std::time::{Duration, Instant}; + +use octosense_wasm_host::{Limits, Runtime}; +use serde_json::{json, Value}; + +const MODULE: &[u8] = include_bytes!("../../../apps/wasmlab/bundle/fns/wasmlab.wasm"); + +/// Median of `runs` timings of `f`, in microseconds. +fn median_us(runs: usize, mut f: impl FnMut()) -> f64 { + let mut times: Vec = (0..runs) + .map(|_| { + let t = Instant::now(); + f(); + t.elapsed() + }) + .collect(); + times.sort(); + times[runs / 2].as_secs_f64() * 1e6 +} + +fn markdown() -> String { + let mut text = String::new(); + for i in 0..200 { + text.push_str(&format!( + "## Section {i}\n\nSome *emphasis*, **strong** text, `code` and a [link](https://example.org/{i}).\n\n- [x] done {i}\n- [ ] todo {i}\n\n| key | value |\n|---|---|\n| a{i} | {i} |\n\n" + )); + } + text +} + +fn day() -> Value { + let busy: Vec<[String; 2]> = (0..30) + .map(|i| { + let start = 8 * 60 + i * 19; + [format!("{:02}:{:02}", start / 60, start % 60), format!("{:02}:{:02}", (start + 11) / 60, (start + 11) % 60)] + }) + .collect(); + json!({"day_start": "08:00", "day_end": "19:00", "duration": 30, "step": 5, "busy": busy, "max": 50}) +} + +fn items() -> Value { + let words = ["calendar", "calculator", "camera", "clock", "mail", "maps", "notes", "news", "photos", "weather"]; + let items: Vec = (0..500).map(|i| format!("{} {} {i}", words[i % 10], words[(i * 7) % 10])).collect(); + json!({"query": "cal not", "items": items, "limit": 20}) +} + +fn texts() -> Value { + let old: Vec = (0..1000).map(|i| format!("line {i}: the quick brown fox")).collect(); + let mut new = old.clone(); + for i in (0..1000).step_by(50) { + new[i] = format!("line {i}: the quick red fox"); + } + new.insert(500, "an inserted line".into()); + json!({"old": old.join("\n"), "new": new.join("\n"), "context": 3}) +} + +fn main() { + println!("module: {} KiB", MODULE.len() / 1024); + let cache = std::env::temp_dir().join(format!("octosense-wasm-measure-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&cache); + + let t = Instant::now(); + let runtime = Runtime::new(Limits::default(), Some(cache.clone())).unwrap(); + let program = runtime.load(MODULE).unwrap(); + println!("compile (Cranelift, first load): {:.1} ms", t.elapsed().as_secs_f64() * 1e3); + let again = Runtime::new(Limits::default(), Some(cache.clone())).unwrap(); + let t = Instant::now(); + let cached = again.load(MODULE).unwrap(); + println!("load from the cache: {:.2} ms (from cache: {})", t.elapsed().as_secs_f64() * 1e3, cached.from_cache()); + let t = Instant::now(); + let mut guest = runtime.instantiate(&program).unwrap(); + println!("instantiate: {:.3} ms", t.elapsed().as_secs_f64() * 1e3); + let _ = std::fs::remove_dir_all(&cache); + + let md = markdown(); + let (day, items, texts) = (day(), items(), texts()); + let json_in = |v: &Value| serde_json::to_vec(v).unwrap(); + + println!("\n{:<44} {:>10} {:>10} {:>7}", "per call (median of 200)", "wasm us", "native us", "ratio"); + let rows: Vec<(String, f64, f64)> = vec![ + ( + format!("md_to_html ({} KiB of Markdown)", md.len() / 1024), + median_us(200, || drop(guest.call("md_to_html", md.as_bytes()).unwrap())), + median_us(200, || drop(wasmlab_functions::md_to_html(md.as_bytes()).unwrap())), + ), + ( + "find_slots (30 busy intervals, 5-min grid)".into(), + median_us(200, || drop(guest.call("find_slots", &json_in(&day)).unwrap())), + median_us(200, || drop(serde_json::to_vec(&wasmlab_functions::find_slots(serde_json::from_slice(&json_in(&day)).unwrap()).unwrap()).unwrap())), + ), + ( + "fuzzy_rank (500 items)".into(), + median_us(200, || drop(guest.call("fuzzy_rank", &json_in(&items)).unwrap())), + median_us(200, || drop(serde_json::to_vec(&wasmlab_functions::fuzzy_rank(serde_json::from_slice(&json_in(&items)).unwrap()).unwrap()).unwrap())), + ), + ( + "text_diff (1000 lines, 21 changes)".into(), + median_us(200, || drop(guest.call("text_diff", &json_in(&texts)).unwrap())), + median_us(200, || drop(serde_json::to_vec(&wasmlab_functions::text_diff(serde_json::from_slice(&json_in(&texts)).unwrap()).unwrap()).unwrap())), + ), + ]; + for (name, wasm, native) in rows { + println!("{name:<44} {wasm:>10.1} {native:>10.1} {:>6.1}x", wasm / native); + } + let empty = json!({"query": "x", "items": [], "limit": 1}); + let call = median_us(2000, || drop(guest.call("fuzzy_rank", &json_in(&empty)).unwrap())); + println!("\nsmallest call (fuzzy_rank over no items, JSON both ways): {call:.1} us"); +} diff --git a/crates/wasm-host/src/lib.rs b/crates/wasm-host/src/lib.rs new file mode 100644 index 00000000..364fa47b --- /dev/null +++ b/crates/wasm-host/src/lib.rs @@ -0,0 +1,570 @@ +//! Developers' own functions, compiled to WebAssembly, run for their app in +//! Wasmtime (Cranelift): light computing and algorithms an app ships beside +//! its script (ADR 0011). +//! +//! # The guest ABI +//! +//! A module is core WebAssembly (the guest crate `octosense-guest` writes all +//! of this for a Rust function): +//! - it exports `memory`, `octo_alloc(len: i32) -> i32` and +//! `octo_free(ptr: i32, len: i32)`; +//! - every other export of type `(i32, i32) -> i64` is a function the app may +//! call: it takes its input bytes at `(ptr, len)` and returns +//! `(out_ptr << 32) | out_len` of a buffer it allocated, which holds a +//! status byte (`0` ok, `1` error) and then the output, or the error text; +//! - its only import may be `octo.log(ptr: i32, len: i32)`. +//! +//! # Containment +//! +//! A module reaches nothing but its own memory and `octo.log`: no file, +//! network, clock or other import exists, and a module that asks for one is +//! refused when it loads. Every call has a deadline (Wasmtime's epoch +//! interruption), the memory and the wasm stack have caps, and input and +//! output have a size limit. A trap, a deadline or a limit ends the call +//! with an error; it never ends the process. The instance is spent then +//! (its module's state may be half-updated) and refuses further calls; a +//! fresh instance of the same program costs a fraction of a millisecond. + +use std::collections::HashMap; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use sha2::{Digest, Sha256}; +use wasmtime::{ + Caller, Config, Engine, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, Trap, + TypedFunc, ValType, +}; + +/// How often the epoch advances: the granularity of a deadline. +const TICK: Duration = Duration::from_millis(10); + +/// The most one `octo.log` line may hold; longer ones are cut. +const LOG_LINE: usize = 1024; + +/// The most log lines an instance keeps until they are read. +const LOG_LINES: usize = 64; + +/// What every instance of a [`Runtime`] may use. +#[derive(Clone, Debug)] +pub struct Limits { + /// How long one call may run. + pub deadline: Duration, + /// The most linear memory an instance may have. + pub memory_bytes: usize, + /// The largest module the runtime loads. + pub module_bytes: usize, + /// The largest input, and the largest output, of a call. + pub io_bytes: usize, + /// The wasm stack of a call. + pub stack_bytes: usize, +} + +impl Default for Limits { + fn default() -> Self { + Limits { + deadline: Duration::from_secs(2), + memory_bytes: 256 << 20, + module_bytes: 8 << 20, + io_bytes: 16 << 20, + stack_bytes: 512 << 10, + } + } +} + +/// Why a module cannot load. +#[derive(Debug, Clone, PartialEq)] +pub enum LoadError { + /// It is bigger than [`Limits::module_bytes`]. + TooLarge(usize), + /// It is not valid WebAssembly for this engine. + Invalid(String), + /// It imports something other than `octo.log`. + Import(String), + /// It lacks `memory`, `octo_alloc` or `octo_free`, or one has the wrong type. + Export(String), +} + +impl fmt::Display for LoadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LoadError::TooLarge(n) => write!(f, "the module is {n} bytes, over the limit"), + LoadError::Invalid(why) => write!(f, "not a valid module: {why}"), + LoadError::Import(what) => { + write!(f, "the module imports {what}; only octo.log is provided") + } + LoadError::Export(what) => write!(f, "the module does not export {what}"), + } + } +} + +impl std::error::Error for LoadError {} + +/// Why a call failed. +#[derive(Debug, Clone, PartialEq)] +pub enum CallError { + /// The module has no such function. + NoSuchFunction(String), + /// The call ran past [`Limits::deadline`]. + Deadline, + /// Its input or output is bigger than [`Limits::io_bytes`]. + TooLarge(usize), + /// The function trapped: a panic, an out-of-bounds access, a stack + /// overflow, memory over its cap. The text says which. + Trap(String), + /// The function returned an error of its own. + Guest(String), + /// An earlier call on this instance trapped or ran past its deadline, so + /// it takes no more calls: instantiate the program again. + Spent, +} + +impl fmt::Display for CallError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CallError::NoSuchFunction(name) => write!(f, "no function {name}"), + CallError::Deadline => write!(f, "the call ran past its deadline"), + CallError::TooLarge(n) => write!(f, "{n} bytes is over the input/output limit"), + CallError::Trap(why) => write!(f, "the function trapped: {why}"), + CallError::Guest(why) => write!(f, "{why}"), + CallError::Spent => write!(f, "the instance stopped after an earlier trap"), + } + } +} + +impl std::error::Error for CallError {} + +/// The engine and the limits every instance shares. Its epoch ticker runs +/// for as long as the runtime or any of its instances lives: an instance +/// never outlives its deadline. +pub struct Runtime { + engine: Engine, + limits: Limits, + cache_dir: Option, + ticker: Arc, +} + +/// A loaded module: checked, compiled (or taken from the cache). +#[derive(Clone)] +pub struct Program { + module: Module, + functions: Vec, + from_cache: bool, +} + +impl Program { + /// The functions an app may call, sorted. + pub fn functions(&self) -> &[String] { + &self.functions + } + + /// Whether the compiled code came from the cache. + pub fn from_cache(&self) -> bool { + self.from_cache + } +} + +struct State { + limits: StoreLimits, + logs: Vec, +} + +/// One app's instance of a [`Program`]: its own memory and its own state. +/// It may move to another thread. +pub struct Instance { + store: Store, + memory: Memory, + alloc: TypedFunc, + free: TypedFunc<(i32, i32), ()>, + functions: HashMap>, + deadline_ticks: u64, + io_bytes: usize, + /// A call trapped or ran past its deadline. The module's own state may + /// be half-updated then (a Rust guest's stack pointer and allocator live + /// in its memory and globals), so the instance takes no more calls. + spent: bool, + _ticker: Arc, +} + +impl Runtime { + /// A runtime with these limits. Compiled code is cached in `cache_dir` + /// when one is given (keyed by the module's digest and the engine). + pub fn new(limits: Limits, cache_dir: Option) -> Result { + let mut config = Config::new(); + config.epoch_interruption(true); + config.max_wasm_stack(limits.stack_bytes); + let engine = Engine::new(&config).map_err(|e| format!("{e:#}"))?; + let ticker = Arc::new(Ticker::start(engine.clone())); + Ok(Runtime { + engine, + limits, + cache_dir, + ticker, + }) + } + + pub fn limits(&self) -> &Limits { + &self.limits + } + + /// Checks and compiles `bytes`, or takes its compiled code from the cache. + pub fn load(&self, bytes: &[u8]) -> Result { + if bytes.len() > self.limits.module_bytes { + return Err(LoadError::TooLarge(bytes.len())); + } + let cached = self.cache_path(bytes); + let mut from_cache = false; + let module = match cached.as_ref().and_then(|path| std::fs::read(path).ok()) { + // SAFETY: the cache holds only what `Module::serialize` wrote for + // this engine (the key includes its compatibility hash), in a + // directory that belongs to the shell. + Some(code) => match unsafe { Module::deserialize(&self.engine, &code) } { + Ok(module) => { + from_cache = true; + module + } + Err(_) => self.compile(bytes, cached.as_ref())?, + }, + None => self.compile(bytes, cached.as_ref())?, + }; + let functions = check(&module)?; + Ok(Program { + module, + functions, + from_cache, + }) + } + + fn compile(&self, bytes: &[u8], cache: Option<&PathBuf>) -> Result { + let module = + Module::new(&self.engine, bytes).map_err(|e| LoadError::Invalid(format!("{e:#}")))?; + if let Some(path) = cache { + // Check before caching: a refused module is never stored. + check(&module)?; + if let Ok(code) = module.serialize() { + let tmp = path.with_extension("tmp"); + if std::fs::write(&tmp, &code).is_ok() && std::fs::rename(&tmp, path).is_ok() { + // Read it back off this thread: an on-access scanner + // (Microsoft Defender's, on a managed Mac) holds the + // first open of a new file for about a second, so let + // that happen now rather than at the next load. + let path = path.clone(); + let _ = std::thread::Builder::new() + .name("wasm-cache".into()) + .spawn(move || drop(std::fs::read(path))); + } + } + } + Ok(module) + } + + fn cache_path(&self, bytes: &[u8]) -> Option { + let dir = self.cache_dir.as_ref()?; + std::fs::create_dir_all(dir).ok()?; + let mut engine = std::collections::hash_map::DefaultHasher::new(); + self.engine + .precompile_compatibility_hash() + .hash(&mut engine); + let digest = Sha256::digest(bytes); + let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect(); + Some(dir.join(format!("{hex}-{:016x}.cwasm", engine.finish()))) + } + + /// A fresh instance of `program`, with its own memory. + pub fn instantiate(&self, program: &Program) -> Result { + let limits = StoreLimitsBuilder::new() + .memory_size(self.limits.memory_bytes) + .instances(1) + .memories(1) + .tables(1) + .trap_on_grow_failure(true) + .build(); + let mut store = Store::new( + &self.engine, + State { + limits, + logs: Vec::new(), + }, + ); + store.limiter(|state| &mut state.limits); + store.epoch_deadline_trap(); + // Instantiation runs the start function and data initialisers: a + // deadline covers it too. + let deadline_ticks = self + .limits + .deadline + .as_millis() + .div_ceil(TICK.as_millis()) + .max(1) as u64; + store.set_epoch_deadline(deadline_ticks); + let mut linker = Linker::new(&self.engine); + linker + .func_wrap( + "octo", + "log", + |mut caller: Caller<'_, State>, ptr: i32, len: i32| { + let Some(memory) = caller.get_export("memory").and_then(|e| e.into_memory()) + else { + return; + }; + let len = (len.max(0) as usize).min(LOG_LINE); + let mut buf = vec![0u8; len]; + if memory.read(&caller, ptr as u32 as usize, &mut buf).is_ok() + && caller.data().logs.len() < LOG_LINES + { + let line = String::from_utf8_lossy(&buf).into_owned(); + caller.data_mut().logs.push(line); + } + }, + ) + .map_err(|e| LoadError::Invalid(format!("{e:#}")))?; + let instance = linker + .instantiate(&mut store, &program.module) + .map_err(|e| LoadError::Invalid(format!("{e:#}")))?; + let memory = instance + .get_memory(&mut store, "memory") + .ok_or_else(|| LoadError::Export("memory".into()))?; + let alloc = instance + .get_typed_func::(&mut store, "octo_alloc") + .map_err(|_| LoadError::Export("octo_alloc(i32) -> i32".into()))?; + let free = instance + .get_typed_func::<(i32, i32), ()>(&mut store, "octo_free") + .map_err(|_| LoadError::Export("octo_free(i32, i32)".into()))?; + let mut functions = HashMap::new(); + for name in &program.functions { + let f = instance + .get_typed_func::<(i32, i32), i64>(&mut store, name) + .map_err(|_| LoadError::Export(format!("{name}(i32, i32) -> i64")))?; + functions.insert(name.clone(), f); + } + Ok(Instance { + store, + memory, + alloc, + free, + functions, + deadline_ticks, + io_bytes: self.limits.io_bytes, + spent: false, + _ticker: self.ticker.clone(), + }) + } +} + +/// The imports and exports a module must have; returns its functions. +fn check(module: &Module) -> Result, LoadError> { + for import in module.imports() { + let ok = import.module() == "octo" + && import.name() == "log" + && import.ty().func().is_some_and(|f| { + f.params().map(|t| t.is_i32()).eq([true, true]) && f.results().len() == 0 + }); + if !ok { + return Err(LoadError::Import(format!( + "{}.{}", + import.module(), + import.name() + ))); + } + } + let mut functions = Vec::new(); + let mut memory = false; + let mut alloc = false; + let mut free = false; + for export in module.exports() { + let ty = export.ty(); + match export.name() { + "memory" => memory = ty.memory().is_some(), + "octo_alloc" => { + alloc = ty + .func() + .is_some_and(|f| sig(f.params(), f.results(), &[ValType::I32], &[ValType::I32])) + } + "octo_free" => { + free = ty.func().is_some_and(|f| { + sig(f.params(), f.results(), &[ValType::I32, ValType::I32], &[]) + }) + } + name => { + if ty.func().is_some_and(|f| { + sig( + f.params(), + f.results(), + &[ValType::I32, ValType::I32], + &[ValType::I64], + ) + }) { + functions.push(name.to_string()); + } + } + } + } + for (have, what) in [ + (memory, "memory"), + (alloc, "octo_alloc(i32) -> i32"), + (free, "octo_free(i32, i32)"), + ] { + if !have { + return Err(LoadError::Export(what.into())); + } + } + functions.sort(); + Ok(functions) +} + +fn sig( + params: impl ExactSizeIterator, + results: impl ExactSizeIterator, + want_params: &[ValType], + want_results: &[ValType], +) -> bool { + let same = |a: ValType, b: &ValType| { + matches!( + (a, b), + (ValType::I32, ValType::I32) | (ValType::I64, ValType::I64) + ) + }; + params.len() == want_params.len() + && results.len() == want_results.len() + && params.zip(want_params).all(|(a, b)| same(a, b)) + && results.zip(want_results).all(|(a, b)| same(a, b)) +} + +impl Instance { + /// The functions this instance has, sorted. + pub fn functions(&self) -> Vec { + let mut names: Vec = self.functions.keys().cloned().collect(); + names.sort(); + names + } + + /// Calls `name` with `input` and returns its output. After a trap or a + /// deadline the instance is spent ([`CallError::Spent`]): instantiate + /// the program again, which is cheap. A function's own error leaves the + /// instance as it was. + pub fn call(&mut self, name: &str, input: &[u8]) -> Result, CallError> { + if self.spent { + return Err(CallError::Spent); + } + let f = self + .functions + .get(name) + .cloned() + .ok_or_else(|| CallError::NoSuchFunction(name.into()))?; + if input.len() > self.io_bytes { + return Err(CallError::TooLarge(input.len())); + } + let result = self.run(f, input); + if matches!(result, Err(CallError::Deadline | CallError::Trap(_))) { + self.spent = true; + } + result + } + + /// Whether a call trapped or ran past its deadline ([`Instance::call`]). + pub fn spent(&self) -> bool { + self.spent + } + + fn run(&mut self, f: TypedFunc<(i32, i32), i64>, input: &[u8]) -> Result, CallError> { + self.store.set_epoch_deadline(self.deadline_ticks); + let len = input.len() as i32; + let ptr = self.alloc.call(&mut self.store, len).map_err(trap)?; + self.memory + .write(&mut self.store, ptr as u32 as usize, input) + .map_err(|_| CallError::Trap("octo_alloc returned memory out of bounds".into()))?; + let packed = f.call(&mut self.store, (ptr, len)).map_err(trap)?; + self.free.call(&mut self.store, (ptr, len)).map_err(trap)?; + let out_ptr = (packed as u64 >> 32) as usize; + let out_len = (packed as u64 & 0xffff_ffff) as usize; + if out_len > self.io_bytes + 1 { + self.free + .call(&mut self.store, (out_ptr as i32, out_len as i32)) + .map_err(trap)?; + return Err(CallError::TooLarge(out_len)); + } + let mut out = vec![0u8; out_len]; + self.memory + .read(&self.store, out_ptr, &mut out) + .map_err(|_| CallError::Trap("the output is out of bounds".into()))?; + self.free + .call(&mut self.store, (out_ptr as i32, out_len as i32)) + .map_err(trap)?; + match out.first() { + Some(0) => { + out.remove(0); + Ok(out) + } + Some(1) => Err(CallError::Guest( + String::from_utf8_lossy(&out[1..]).into_owned(), + )), + _ => Err(CallError::Trap("the output has no status byte".into())), + } + } + + /// The lines the module logged since the last read. + pub fn take_logs(&mut self) -> Vec { + std::mem::take(&mut self.store.data_mut().logs) + } + + /// The instance's linear memory, in bytes. + pub fn memory_bytes(&self) -> usize { + self.memory.data_size(&self.store) + } +} + +fn trap(error: wasmtime::Error) -> CallError { + match error.downcast_ref::() { + Some(Trap::Interrupt) => CallError::Deadline, + Some(trap) => CallError::Trap(trap.to_string()), + // A limit the store enforces: say which, without the wasm backtrace + // wrapped around it. + None => { + let cause = error.root_cause().to_string(); + if cause.contains("growing memory") { + CallError::Trap(format!("memory over its cap ({cause})")) + } else { + CallError::Trap(cause) + } + } + } +} + +/// Advances the engine's epoch every [`TICK`] until dropped. +struct Ticker { + stop: Arc, +} + +impl Ticker { + fn start(engine: Engine) -> Ticker { + let stop = Arc::new(AtomicBool::new(false)); + let flag = stop.clone(); + std::thread::Builder::new() + .name("wasm-epoch".into()) + .spawn(move || { + // Advance by the time that has passed, not by the sleeps: a + // sleep can overrun, and a deadline is wall-clock time. + let started = Instant::now(); + let mut ticks = 0u128; + while !flag.load(Ordering::Relaxed) { + std::thread::sleep(TICK); + let due = started.elapsed().as_nanos() / TICK.as_nanos(); + while ticks < due { + engine.increment_epoch(); + ticks += 1; + } + } + }) + .expect("the wasm epoch thread"); + Ticker { stop } + } +} + +impl Drop for Ticker { + fn drop(&mut self) { + self.stop.store(true, Ordering::Relaxed); + } +} diff --git a/crates/wasm-host/tests/guest.rs b/crates/wasm-host/tests/guest.rs new file mode 100644 index 00000000..aaf5cd2a --- /dev/null +++ b/crates/wasm-host/tests/guest.rs @@ -0,0 +1,170 @@ +//! Wasm Lab's real Rust functions (apps/wasmlab/guest, built into +//! apps/wasmlab/bundle/fns/wasmlab.wasm by its build.sh) in the runtime: +//! each gives exactly what the same code gives natively, and each way the +//! rogue function misbehaves ends in an error. + +use std::time::{Duration, Instant}; + +use octosense_wasm_host::{CallError, Instance, Limits, Program, Runtime}; +use serde_json::{json, Value}; + +const MODULE: &[u8] = include_bytes!("../../../apps/wasmlab/bundle/fns/wasmlab.wasm"); + +fn lab() -> (Runtime, Program) { + let runtime = Runtime::new( + Limits { + deadline: Duration::from_millis(500), + memory_bytes: 64 << 20, + ..Limits::default() + }, + None, + ) + .unwrap(); + let program = runtime.load(MODULE).unwrap(); + (runtime, program) +} + +fn guest() -> Instance { + let (runtime, program) = lab(); + // The instance outlives the runtime, and keeps its deadline. + runtime.instantiate(&program).unwrap() +} + +fn call_json(guest: &mut Instance, name: &str, input: Value) -> Value { + let out = guest + .call(name, &serde_json::to_vec(&input).unwrap()) + .unwrap(); + serde_json::from_slice(&out).unwrap() +} + +#[test] +fn the_module_offers_its_functions_and_imports_only_log() { + assert_eq!( + guest().functions(), + [ + "find_slots", + "fuzzy_rank", + "md_to_html", + "rogue", + "text_diff" + ] + ); +} + +#[test] +fn markdown_matches_native() { + let text = "# Notes\n\nSome *emphasis*, `code` and a [link](https://example.org).\n\n- [x] done\n- [ ] todo\n\n| a | b |\n|---|---|\n| 1 | 2 |\n"; + let wasm = guest().call("md_to_html", text.as_bytes()).unwrap(); + assert_eq!( + wasm, + wasmlab_functions::md_to_html(text.as_bytes()).unwrap() + ); +} + +#[test] +fn slots_match_native() { + let input = json!({"day_start": "08:30", "day_end": "18:00", "duration": 45, "step": 15, + "busy": [["09:00", "10:00"], ["09:45", "11:15"], ["13:00", "14:00"], ["16:50", "17:05"]], "max": 20}); + let wasm = call_json(&mut guest(), "find_slots", input.clone()); + let native = wasmlab_functions::find_slots(serde_json::from_value(input).unwrap()).unwrap(); + assert_eq!(wasm, serde_json::to_value(native).unwrap()); + assert_eq!(wasm["slots"][0], json!(["11:15", "12:00"])); +} + +#[test] +fn ranking_matches_native() { + let items = [ + "Calendar", + "Calculator", + "Camera", + "Local calls", + "Mail", + "Clock", + "Weather", + "Cal Newport notes", + ]; + let input = json!({"query": "cal", "items": items, "limit": 5}); + let wasm = call_json(&mut guest(), "fuzzy_rank", input.clone()); + let native = wasmlab_functions::fuzzy_rank(serde_json::from_value(input).unwrap()).unwrap(); + assert_eq!(wasm, serde_json::to_value(native).unwrap()); +} + +#[test] +fn diff_matches_native() { + let input = json!({"old": "one\ntwo\nthree\nfour\n", "new": "one\n2\nthree\nfour\nfive\n", "context": 1}); + let wasm = call_json(&mut guest(), "text_diff", input.clone()); + let native = wasmlab_functions::text_diff(serde_json::from_value(input).unwrap()).unwrap(); + assert_eq!(wasm, serde_json::to_value(native).unwrap()); +} + +#[test] +fn bad_input_is_the_functions_own_error() { + let error = guest() + .call("find_slots", b"{\"day_start\": 9}") + .unwrap_err(); + assert!( + matches!(&error, CallError::Guest(why) if why.contains("find_slots")), + "{error:?}" + ); + let error = guest() + .call( + "find_slots", + br#"{"day_start": "18:00", "day_end": "09:00", "duration": 30}"#, + ) + .unwrap_err(); + assert_eq!( + error, + CallError::Guest("the day ends before it starts".into()) + ); +} + +#[test] +fn rogue_code_ends_in_an_error_and_spends_only_its_own_instance() { + let (runtime, program) = lab(); + let rogue = |mode: &str| { + let mut guest = runtime.instantiate(&program).unwrap(); + let error = guest + .call("rogue", json!({ "mode": mode }).to_string().as_bytes()) + .unwrap_err(); + assert!(guest.spent(), "{mode}"); + assert_eq!( + guest.call("fuzzy_rank", br#"{"query": "m", "items": []}"#), + Err(CallError::Spent) + ); + (error, guest) + }; + + let started = Instant::now(); + assert_eq!(rogue("loop").0, CallError::Deadline); + assert!(started.elapsed() < Duration::from_secs(2)); + + let (error, guest) = rogue("alloc"); + assert!( + matches!(&error, CallError::Trap(why) if why.starts_with("memory over its cap") && !why.contains("backtrace")), + "{error:?}" + ); + assert!(guest.memory_bytes() <= 64 << 20); + + let (error, mut guest) = rogue("panic"); + assert!(matches!(&error, CallError::Trap(_)), "{error:?}"); + let logs = guest.take_logs(); + assert!( + logs.iter().any(|l| l.contains("rogue: a deliberate panic")), + "{logs:?}" + ); + + let (error, _) = rogue("recurse"); + assert!( + matches!(&error, CallError::Trap(why) if why.contains("stack")), + "{error:?}" + ); + + // A fresh instance of the same program answers after all of it. + let mut fresh = runtime.instantiate(&program).unwrap(); + let out = call_json( + &mut fresh, + "fuzzy_rank", + json!({"query": "mail", "items": ["Mail", "Maps"]}), + ); + assert_eq!(out["ranked"][0]["item"], "Mail"); +} diff --git a/crates/wasm-host/tests/runtime.rs b/crates/wasm-host/tests/runtime.rs new file mode 100644 index 00000000..0bf449b7 --- /dev/null +++ b/crates/wasm-host/tests/runtime.rs @@ -0,0 +1,255 @@ +//! The runtime against a hand-written guest that follows the ABI: a bump +//! allocator, a function that works, and functions that misbehave in each +//! way the containment has to stop. + +use std::time::{Duration, Instant}; + +use octosense_wasm_host::{CallError, Instance, Limits, LoadError, Runtime}; + +const GUEST: &str = r#" +(module + (import "octo" "log" (func $log (param i32 i32))) + (memory (export "memory") 1) + (global $heap (mut i32) (i32.const 1024)) + (global $count (mut i32) (i32.const 0)) + (data (i32.const 16) "\01nope") + (data (i32.const 32) "\00") + + (func $alloc (export "octo_alloc") (param $len i32) (result i32) + (local $p i32) + (local.set $p (global.get $heap)) + (block $fits + (br_if $fits (i32.le_u (i32.add (local.get $p) (local.get $len)) + (i32.mul (memory.size) (i32.const 65536)))) + (drop (memory.grow (i32.add (i32.shr_u (local.get $len) (i32.const 16)) (i32.const 1))))) + (global.set $heap (i32.and (i32.add (i32.add (local.get $p) (local.get $len)) (i32.const 7)) + (i32.const -8))) + (local.get $p)) + + (func (export "octo_free") (param i32 i32)) + + (func $pack (param $p i32) (param $n i32) (result i64) + (i64.or (i64.shl (i64.extend_i32_u (local.get $p)) (i64.const 32)) + (i64.extend_i32_u (local.get $n)))) + + ;; The input in upper case. + (func (export "upper") (param $ptr i32) (param $len i32) (result i64) + (local $out i32) (local $i i32) (local $c i32) + (local.set $out (call $alloc (i32.add (local.get $len) (i32.const 1)))) + (i32.store8 (local.get $out) (i32.const 0)) + (block $done + (loop $next + (br_if $done (i32.ge_u (local.get $i) (local.get $len))) + (local.set $c (i32.load8_u (i32.add (local.get $ptr) (local.get $i)))) + (if (i32.and (i32.ge_u (local.get $c) (i32.const 97)) (i32.le_u (local.get $c) (i32.const 122))) + (then (local.set $c (i32.sub (local.get $c) (i32.const 32))))) + (i32.store8 (i32.add (i32.add (local.get $out) (i32.const 1)) (local.get $i)) (local.get $c)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $next))) + (call $pack (local.get $out) (i32.add (local.get $len) (i32.const 1)))) + + ;; An error of the guest's own. + (func (export "fail") (param i32 i32) (result i64) + (call $pack (i32.const 16) (i32.const 5))) + + ;; An endless loop. + (func (export "spin") (param i32 i32) (result i64) + (loop $forever (br $forever)) + (unreachable)) + + ;; A panic. + (func (export "boom") (param i32 i32) (result i64) + (unreachable)) + + ;; Runaway recursion. + (func $deep (export "deep") (param i32 i32) (result i64) + (call $deep (local.get 0) (local.get 1))) + + ;; A gigabyte more memory. + (func (export "hog") (param i32 i32) (result i64) + (drop (memory.grow (i32.const 16384))) + (call $pack (i32.const 32) (i32.const 1))) + + ;; Logs its input. + (func (export "say") (param $ptr i32) (param $len i32) (result i64) + (call $log (local.get $ptr) (local.get $len)) + (call $pack (i32.const 32) (i32.const 1))) + + ;; How many times it was called, as one digit. + (func (export "count") (param i32 i32) (result i64) + (global.set $count (i32.add (global.get $count) (i32.const 1))) + (i32.store8 (i32.const 40) (i32.const 0)) + (i32.store8 (i32.const 41) (i32.add (i32.const 48) (global.get $count))) + (call $pack (i32.const 40) (i32.const 2)))) +"#; + +fn limits() -> Limits { + Limits { + deadline: Duration::from_millis(200), + memory_bytes: 64 << 20, + ..Limits::default() + } +} + +fn instance(runtime: &Runtime) -> Instance { + let program = runtime.load(&wat::parse_str(GUEST).unwrap()).unwrap(); + runtime.instantiate(&program).unwrap() +} + +#[test] +fn a_function_gets_its_input_and_returns_its_output() { + let runtime = Runtime::new(limits(), None).unwrap(); + let mut guest = instance(&runtime); + assert_eq!( + guest.functions(), + ["boom", "count", "deep", "fail", "hog", "say", "spin", "upper"] + ); + assert_eq!(guest.call("upper", b"hello, Octo").unwrap(), b"HELLO, OCTO"); + assert_eq!(guest.call("upper", b"").unwrap(), b""); + assert_eq!( + guest.call("nope", b""), + Err(CallError::NoSuchFunction("nope".into())) + ); +} + +#[test] +fn a_guest_error_comes_back_as_its_text_and_the_instance_goes_on() { + let runtime = Runtime::new(limits(), None).unwrap(); + let mut guest = instance(&runtime); + assert_eq!(guest.call("fail", b""), Err(CallError::Guest("nope".into()))); + assert!(!guest.spent()); + assert_eq!(guest.call("upper", b"still here").unwrap(), b"STILL HERE"); +} + +#[test] +fn an_endless_loop_stops_at_its_deadline_and_spends_the_instance() { + let runtime = Runtime::new(limits(), None).unwrap(); + let program = runtime.load(&wat::parse_str(GUEST).unwrap()).unwrap(); + let mut guest = runtime.instantiate(&program).unwrap(); + let started = Instant::now(); + assert_eq!(guest.call("spin", b""), Err(CallError::Deadline)); + let took = started.elapsed(); + // The deadline is 200 ms of wall-clock time, give or take a tick. + assert!( + took >= Duration::from_millis(190) && took < Duration::from_millis(400), + "stopped after {took:?}" + ); + // Its state may be half-updated: it takes no more calls, and a fresh + // instance of the same program does. + assert!(guest.spent()); + assert_eq!(guest.call("upper", b"x"), Err(CallError::Spent)); + let mut fresh = runtime.instantiate(&program).unwrap(); + assert_eq!(fresh.call("upper", b"still here").unwrap(), b"STILL HERE"); +} + +#[test] +fn memory_beyond_the_cap_ends_the_call() { + let runtime = Runtime::new(limits(), None).unwrap(); + let mut guest = instance(&runtime); + assert!(matches!(guest.call("hog", b""), Err(CallError::Trap(_)))); + assert!(guest.memory_bytes() <= 64 << 20); +} + +#[test] +fn a_trap_is_an_error_not_a_crash() { + let runtime = Runtime::new(limits(), None).unwrap(); + let mut guest = instance(&runtime); + let error = guest.call("boom", b"").unwrap_err(); + assert!( + matches!(&error, CallError::Trap(why) if why.contains("unreachable")), + "{error:?}" + ); + assert_eq!(guest.call("upper", b"x"), Err(CallError::Spent)); +} + +#[test] +fn runaway_recursion_traps_at_the_stack_cap() { + let runtime = Runtime::new(limits(), None).unwrap(); + let error = instance(&runtime).call("deep", b"").unwrap_err(); + assert!( + matches!(&error, CallError::Trap(why) if why.contains("stack")), + "{error:?}" + ); +} + +#[test] +fn log_is_the_only_import() { + let runtime = Runtime::new(limits(), None).unwrap(); + let mut guest = instance(&runtime); + guest.call("say", b"hi from the guest").unwrap(); + assert_eq!(guest.take_logs(), ["hi from the guest"]); + let wasi = r#"(module + (import "wasi_snapshot_preview1" "fd_write" (func (param i32 i32 i32 i32) (result i32))) + (memory (export "memory") 1) + (func (export "octo_alloc") (param i32) (result i32) (i32.const 0)) + (func (export "octo_free") (param i32 i32)))"#; + assert_eq!( + runtime.load(&wat::parse_str(wasi).unwrap()).err(), + Some(LoadError::Import("wasi_snapshot_preview1.fd_write".into())) + ); + let no_alloc = + r#"(module (memory (export "memory") 1) (func (export "octo_free") (param i32 i32)))"#; + assert!(matches!( + runtime.load(&wat::parse_str(no_alloc).unwrap()), + Err(LoadError::Export(_)) + )); +} + +#[test] +fn instances_keep_their_own_state() { + let runtime = Runtime::new(limits(), None).unwrap(); + let (mut a, mut b) = (instance(&runtime), instance(&runtime)); + for _ in 0..3 { + a.call("count", b"").unwrap(); + } + assert_eq!(a.call("count", b"").unwrap(), b"4"); + assert_eq!(b.call("count", b"").unwrap(), b"1"); +} + +#[test] +fn an_instance_can_run_on_another_thread() { + let runtime = Runtime::new(limits(), None).unwrap(); + let mut guest = instance(&runtime); + let out = std::thread::spawn(move || guest.call("upper", b"worker").unwrap()) + .join() + .unwrap(); + assert_eq!(out, b"WORKER"); +} + +#[test] +fn input_over_the_limit_is_refused() { + let runtime = Runtime::new( + Limits { + io_bytes: 16, + ..limits() + }, + None, + ) + .unwrap(); + assert_eq!( + instance(&runtime).call("upper", &[b'a'; 17]), + Err(CallError::TooLarge(17)) + ); +} + +#[test] +fn compiled_code_is_cached_by_digest() { + let dir = + std::env::temp_dir().join(format!("octosense-wasm-host-cache-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let bytes = wat::parse_str(GUEST).unwrap(); + let first = Runtime::new(limits(), Some(dir.clone())).unwrap(); + assert!(!first.load(&bytes).unwrap().from_cache()); + let second = Runtime::new(limits(), Some(dir.clone())).unwrap(); + let program = second.load(&bytes).unwrap(); + assert!(program.from_cache()); + assert_eq!( + second + .instantiate(&program) + .unwrap() + .call("upper", b"cached") + .unwrap(), + b"CACHED" + ); + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index bd9430bd..a470e04e 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -71,5 +71,7 @@ app-aichat = ["octosense-shell/app-aichat"] # Developer mode from Settings and OCTOSENSE_DEV_MODE in a release build # (debug builds have it already; ADR 0004 §13). dev-mode = ["octosense-shell/dev-mode"] +# Apps' own WebAssembly functions (ADR 0011; off by default). +wasm-lab = ["octosense-shell/wasm-lab"] # The system toolbox offered to app agents (off by default; ADR 0002 §6). toolbox-peers = ["octosense-shell/toolbox-peers"] diff --git a/desktop/system-apps-wasm-lab.json b/desktop/system-apps-wasm-lab.json new file mode 100644 index 00000000..c0158fdd --- /dev/null +++ b/desktop/system-apps-wasm-lab.json @@ -0,0 +1,15 @@ +{ + "schema": 1, + "source": "../apps", + "apps": [ + "news", + "photos", + "maps", + "mail", + "calendar", + "ai-providers", + "youtube", + "wasmlab" + ], + "assets": {} +} diff --git a/native-apps.json b/native-apps.json index 28e18406..bebf9363 100644 --- a/native-apps.json +++ b/native-apps.json @@ -100,7 +100,7 @@ "id": "apphub", "feature": "app-hub", "crate": "octosense-app-hub-app", - "source": { "git": "https://github.com/OctoSense-org/OctoSense-App-Hub", "rev": "b1109a397ae45349546d50889a1706f6c58fb923", "local": null }, + "source": { "git": "https://github.com/OctoSense-org/OctoSense-App-Hub", "rev": "2e5768964f15916cdb66cc6824f7671dafc6445a", "local": null }, "module": ["octosense_app_hub_app::APP_HUB_MODULE", "octosense_app_hub_app::CARD_MODULE"], "bin": null, "default_features": true, diff --git a/phone/Cargo.toml b/phone/Cargo.toml index 7710c3c2..cff97330 100644 --- a/phone/Cargo.toml +++ b/phone/Cargo.toml @@ -98,5 +98,7 @@ app-weather = ["octosense-shell/app-weather"] # END native-apps: features app-aichat = ["dep:makepad-aichat", "octosense-shell/app-aichat"] dev-mode = ["octosense-shell/dev-mode"] +# Apps' own WebAssembly functions (ADR 0011; off by default). +wasm-lab = ["octosense-shell/wasm-lab"] # The system toolbox offered to app agents (off by default; ADR 0002 §6). toolbox-peers = ["octosense-shell/toolbox-peers"] diff --git a/phone/system-apps-wasm-lab.json b/phone/system-apps-wasm-lab.json new file mode 100644 index 00000000..6ba0c6bb --- /dev/null +++ b/phone/system-apps-wasm-lab.json @@ -0,0 +1,20 @@ +{ + "schema": 1, + "source": "../apps", + "apps": [ + "news", + "photos", + "maps", + "camera", + "mail", + "calendar", + "ai-providers", + "youtube", + "wasmlab" + ], + "assets": { + "photos": { + "photos": "../apps/photos/resources/photos" + } + } +} diff --git a/tools/sync-app-interface.py b/tools/sync-app-interface.py index dfab7707..c2b72c81 100644 --- a/tools/sync-app-interface.py +++ b/tools/sync-app-interface.py @@ -14,7 +14,7 @@ def main(): args = parser.parse_args() prelude = START + (ROOT / "apps/interface.splash").read_text() + END stale = [] - for name in ("news", "photos", "mail", "calendar", "maps", "ai-providers", "youtube"): + for name in ("news", "photos", "mail", "calendar", "maps", "ai-providers", "youtube", "wasmlab"): path = ROOT / "apps" / name / "bundle/main.splash" text = path.read_text() body = text.split(END, 1)[1] if text.startswith(START) else text From 97fbb1be030c47019eb81e12d65a030d01811135 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:50:14 -0700 Subject: [PATCH 2/2] docs: ADR 0011, an app's own functions in WebAssembly (proposed) The decision record, in English and Chinese: modules in a bundle's fns/, Wasmtime with Cranelift behind the wasm host service and capability, wasm. as an agent tool, the runtimes measured against it (with CoreMark), Wasm Lab's numbers on the desktop and a Redmi Note 12 (in a plain binary and in Home), the libc 0.2.190 trap fix, the desktop scanner's effect on the cache, and what is still open: iOS has no JIT, there is no per-app CPU or memory budget, and only a system app has run functions on a device. Listed beside ADR 0012. Co-Authored-By: Claude Opus 5.5 --- .../0011-apps-own-functions-in-webassembly.md | 161 ++++++++++++++++++ ...apps-own-functions-in-webassembly.zh-CN.md | 127 ++++++++++++++ docs/adr/README.md | 1 + docs/adr/README.zh-CN.md | 1 + 4 files changed, 290 insertions(+) create mode 100644 docs/adr/0011-apps-own-functions-in-webassembly.md create mode 100644 docs/adr/0011-apps-own-functions-in-webassembly.zh-CN.md diff --git a/docs/adr/0011-apps-own-functions-in-webassembly.md b/docs/adr/0011-apps-own-functions-in-webassembly.md new file mode 100644 index 00000000..241d69f6 --- /dev/null +++ b/docs/adr/0011-apps-own-functions-in-webassembly.md @@ -0,0 +1,161 @@ +# ADR 0011: An app's own functions, in WebAssembly + +English | [简体中文](0011-apps-own-functions-in-webassembly.zh-CN.md) + +Status: Proposed. Implemented behind the shell's `wasm-lab` feature (off by +default), with the contract side in App Hub #140 (app-contract 1.7.0). +Validated on macOS in a hidden desktop shell and on an Android +phone (Redmi Note 12) in a separately packaged Home; iOS and OpenHarmony not +tried. + +## Context + +A script app is Splash: contained, interpreted, and limited to what its +runtime offers. Some of an app's logic is better written in Rust: a parser, a +ranking or scheduling algorithm, a diff, a format conversion, or an existing +crate that does exactly the job. Today the only ways to run Rust for an app +are a native app (linked into the shell, fully trusted: not for third +parties) or a host service (the shell's own code, written by us). Splash's +compute kernels cover numeric work, not general Rust. + +We want a developer to ship their own Rust functions inside their bundle, and +the shell to run them for that app only, with no new trust: nothing the +functions can reach that the app could not already reach, and nothing at all +outside their own input and output. + +## Decision + +1. **Modules.** A bundle may carry core WebAssembly modules as `fns/*.wasm` + (at most 8). A module exports `memory`, `octo_alloc(len) -> ptr`, + `octo_free(ptr, len)` and its functions, each `(ptr, len) -> i64` returning + `(out_ptr << 32) | out_len` of an output that starts with a status byte (0 + ok, 1 error). Its only import may be `octo.log(ptr, len)`. A small guest + crate (`apps/wasmlab/guest/octosense-guest`) writes all of this for + `fn(&[u8]) -> Result, String>` and for serde types. No component + model and no WASI in this version. +2. **Runtime.** `crates/wasm-host` runs modules in Wasmtime 49, compiled by + Cranelift when they load. Compiled code is cached in the host directory, + keyed by the module's digest and the engine's compatibility hash. Every + call has a deadline (2 s, epoch interruption against wall-clock time); + memory (256 MiB), the wasm stack (512 KiB), the module (8 MiB) and input + and output (16 MiB) have caps. A trap or a deadline ends the call with an + error, never the process, and spends the instance: the next call gets a + fresh one, because a Rust guest's stack pointer and allocator may be + half-updated. +3. **The `wasm` host service** (shell feature `wasm-lab`). `wasm.` + calls one of the calling app's functions; `wasm.functions` lists them, how + they loaded and how they have run. A string argument goes in as its text, + anything else as JSON; JSON output comes back as data, anything else as + `{"text": …}`. Modules come only from the calling app's own admitted, + digest-checked bundle, never from an argument, and the service checks the + admitted manifest's grant itself. Each app has one worker thread, so no + call runs on the UI thread and a slow function holds up only its own app. +4. **The `wasm` capability** joins App Hub's closed capability list. A store + says: "Runs its own functions in a sandbox on this device; they reach no + files, network or other apps." +5. **Agent tools.** A tool may name `host_method: "wasm."`: the + app's agent (and, if shared, other callers) runs the app's own function. + It needs the `wasm` capability, and unlike shared host methods it has no + risk floor and no `private_data` requirement: the function sees only the + arguments it is given. + +## Alternatives measured + +CoreMark on the M5 Max (higher is faster; native Rust is about 45,000): + +| Runtime | CoreMark | Notes | +| --- | --- | --- | +| Wasmtime + Cranelift (chosen) | 44,534 | 7.4 MiB in the binary; compiles when a module loads | +| WasmEdge AOT | 30,432 | 153 MiB library; 0.2–0.36 s to compile | +| stitch (interpreter) | 2,942 | makepad-friendly, no JIT; needs a host-call fix (makepad #106) | +| Pulley (Wasmtime's interpreter) | 2,587 | no JIT: the iOS option | +| WasmEdge interpreter | 374 | | + +- **Native dynamic libraries** give no isolation; Theseus OS's model (a + trusted compiler, one address space) does not apply to a phone OS. +- **Upstream Makepad's JIT** compiles Splash, not Rust. +- **Splash compute kernels** stay the answer for numeric kernels. + +## Measurements (Wasm Lab, `apps/wasmlab`) + +Light algorithms only: Markdown to HTML (pulldown-cmark), free slots around +busy times, fuzzy ranking (strsim), a line diff (similar). + +Medians of 200 calls, release builds, three runs on the desktop and three on +the phone (`cargo run --release -p octosense-wasm-host --example measure`; +on the phone the same example, cross-compiled and run over adb): + +| | Desktop (M5 Max) | Phone (Redmi Note 12, Snapdragon 685) | +| --- | --- | --- | +| Module | 433 KiB | 433 KiB | +| Compile (first load) | 27–33 ms | 378–421 ms | +| Load from the cache | 1.0–1.4 ms in a new process; 4.8–6.4 ms right after writing | 5.0–7.0 ms | +| Instantiate | 0.07 ms | 0.19–0.27 ms | +| md_to_html, 32 KiB | 405–412 µs (2.1–2.2× native) | 2,325–2,328 µs (1.7×) | +| find_slots, 30 busy intervals | 6.7 µs (1.7×) | 40 µs (1.5×) | +| fuzzy_rank, 500 items | 138–146 µs (1.6–1.7×) | 841–845 µs (1.4×) | +| text_diff, 1000 lines | 231–254 µs (1.6–1.7×) | 1,434 µs (1.9×) | +| Smallest call (JSON both ways) | 0.3 µs | 2.5–2.6 µs | + +Building the guest with wasm SIMD (`+simd128`) did not narrow the Markdown +gap (2.2–2.3×): it is Cranelift's code generation against LLVM's, not +vectorisation. Wasmtime and Cranelift add about 7.5 MiB of code to the +Android library (symbol sizes; 7.4 MiB measured on the desktop). + +In the shell, a script's `host.request` to its function and back takes about +2 ms on the desktop and 1.8–2 ms on the phone, almost all of it the UI event +loop; the function itself takes 18–72 µs on the desktop and 47–190 µs on the +phone. In Home on the phone the module compiled in 670 ms on the first launch +(the app was still starting) and loaded from the cache in 7.4–11 ms after. +The four misbehaving modes (an endless loop, endless allocation, a panic, +runaway recursion) each ended in an error after 2,000 ms, 140 ms, 2.2 ms and +3 ms, and Home kept running. + +Two platform findings: + +- **Android traps needed libc 0.2.190.** With libc 0.2.189 every trap that + Wasmtime catches by signal (a panic's `unreachable`, an out-of-bounds load, + a stack overflow) killed Home, and a plain binary too. Wasmtime's handler + ran but read the faulting pc through libc's `ucontext_t`, whose aarch64 + Android layout lacked the kernel's padding after the signal mask, so no + trap looked like wasm. 0.2.190 fixed the layout; `crates/wasm-host` now + requires it. Explicit traps (`signals_based_traps(false)`) also worked + but cost 19–38% per call and doubled the compile time. +- **A desktop scanner can make the cache slower than compiling.** On a + managed Mac, Microsoft Defender holds the first open of a freshly written + cache file for about a second, and again the first time a new build of the + shell opens it (1,086 ms; 2.3 ms on the next launch). The writer reads the + file back in the background, which covers the first case only. Compiling + takes 30–60 ms there, so a desktop could skip the cache or race the two; + on the phone the cache saves about 0.4 s. + +## Consequences + +- Third-party Rust runs in OctoSense apps at 1.4–2.2× native speed for these + workloads, with no new trust and no change to the default build. +- The Android APK grows by 3.5 MiB (209.1 to 212.8 MB, with Wasm Lab's own + bundle); the runtime is about 7.5 MiB of code. +- Compiling costs about 0.4 s per module on a mid-range phone (0.67 s while + Home is starting). The cache makes later loads 5–11 ms; compiling at + install time (store apps) or at build time (system apps) would remove the + first one. +- A store bundle may carry `fns/*.wasm` under App Hub #140's gate (a core + module, at most 8, only with the `wasm` capability). So far only a system + app has run functions on a device. +- iOS allows no JIT for apps and no downloaded native code: there, Pulley + (about 17× slower than Cranelift). A shipped system app's functions could + be translated to native code when the app is built; a store app's could + not. OpenHarmony's JIT policy is unknown (**unverified**). +- The limits are per call and per instance. Nothing yet stops an app from + keeping one core busy with back-to-back calls, or from using 256 MiB in + each of its eight modules; a per-app CPU and memory budget is follow-up + work. A request that App Hub has timed out still runs in the worker. +- An app updated while the shell runs keeps its old functions until the + shell restarts; workers live as long as the shell. + +## Open questions + +- Typed interfaces (the component model and WIT) instead of JSON in and out. +- Any host import beyond `octo.log` (a clock, randomness) would be a new + capability, not a default. +- Epoch deadlines are cheap but not deterministic; fuel would be, at a cost. diff --git a/docs/adr/0011-apps-own-functions-in-webassembly.zh-CN.md b/docs/adr/0011-apps-own-functions-in-webassembly.zh-CN.md new file mode 100644 index 00000000..0c1bec3d --- /dev/null +++ b/docs/adr/0011-apps-own-functions-in-webassembly.zh-CN.md @@ -0,0 +1,127 @@ +# ADR 0011:应用自带的 WebAssembly 函数 + +[English](0011-apps-own-functions-in-webassembly.md) | 简体中文 + +状态:提议中。在外壳的 `wasm-lab` 特性(默认关闭)之后实现,契约部分在 App Hub #140 +(app-contract 1.7.0)。已在 macOS 的隐藏桌面外壳中,以及一部 +Android 手机(Redmi Note 12)上单独打包的 Home 中验证;iOS 和 OpenHarmony 尚未尝试。 + +## 背景 + +脚本应用使用 Splash:受隔离、解释执行,能力以运行时提供的为限。应用里有些逻辑更适合用 +Rust 写:解析器、排序或排程算法、差异比较、格式转换,或者现成的、正好能完成任务的 crate。 +目前为应用运行 Rust 只有两种方式:原生应用(链接进外壳,完全受信任,不适合第三方)或宿主 +服务(外壳自己的代码,由我们编写)。Splash 的计算内核适合数值计算,不适合通用 Rust。 + +我们希望开发者能把自己的 Rust 函数放进应用包里,由外壳只为这个应用运行,而且不引入新的 +信任:函数能接触的不超过应用本来能接触的,除了自己的输入和输出,什么都接触不到。 + +## 决定 + +1. **模块。** 应用包可以在 `fns/*.wasm` 中携带核心 WebAssembly 模块(最多 8 个)。 + 模块导出 `memory`、`octo_alloc(len) -> ptr`、`octo_free(ptr, len)` 和它的函数; + 每个函数的签名是 `(ptr, len) -> i64`,返回 `(out_ptr << 32) | out_len`,输出的第一个 + 字节是状态(0 成功,1 出错)。唯一允许的导入是 `octo.log(ptr, len)`。一个小的客体 crate + (`apps/wasmlab/guest/octosense-guest`)为 `fn(&[u8]) -> Result, String>` 和 + serde 类型生成这一切。这一版不用组件模型,也不用 WASI。 +2. **运行时。** `crates/wasm-host` 在 Wasmtime 49 中运行模块,模块加载时由 Cranelift + 编译。编译结果缓存在宿主目录中,以模块摘要和引擎兼容性哈希为键。每次调用都有截止时间 + (2 秒,按实际经过的时间做 epoch 中断);内存(256 MiB)、wasm 栈(512 KiB)、模块 + (8 MiB)以及输入输出(16 MiB)都有上限。陷阱或超时只让这次调用出错,绝不结束进程, + 并作废这个实例:下次调用会得到新实例,因为 Rust 客体的栈指针和分配器可能只更新了一半。 +3. **`wasm` 宿主服务**(外壳特性 `wasm-lab`)。`wasm.` 调用发起请求的应用自己的 + 函数;`wasm.functions` 列出这些函数、它们的加载情况和运行统计。字符串参数按文本传入, + 其他参数按 JSON 传入;JSON 输出作为数据返回,其他输出返回为 `{"text": …}`。模块只来自 + 发起请求的应用自己那个已准入、经过摘要校验的应用包,绝不来自参数;服务还会自己检查已准入 + 清单中的授权。每个应用有一个工作线程,所以没有调用在 UI 线程上运行,一个慢函数只会拖慢 + 它自己的应用。 +4. **`wasm` 能力**加入 App Hub 封闭的能力列表。商店的说明是:“在此设备上的沙盒中运行它 + 自带的函数;这些函数接触不到文件、网络或其他应用。” +5. **Agent 工具。** 工具可以写 `host_method: "wasm."`:应用的 Agent(以及在 + 共享时的其他调用者)运行这个应用自己的函数。它需要 `wasm` 能力;与共享宿主方法不同,它 + 没有风险下限,也不要求 `private_data`:函数只能看到传给它的参数。 + +## 实测过的替代方案 + +M5 Max 上的 CoreMark(越高越快;原生 Rust 约 45,000): + +| 运行时 | CoreMark | 说明 | +| --- | --- | --- | +| Wasmtime + Cranelift(选定) | 44,534 | 二进制增加 7.4 MiB;模块加载时编译 | +| WasmEdge AOT | 30,432 | 库 153 MiB;编译 0.2–0.36 秒 | +| stitch(解释器) | 2,942 | 与 Makepad 契合,无 JIT;需要一个宿主调用修复(makepad #106) | +| Pulley(Wasmtime 的解释器) | 2,587 | 无 JIT:iOS 的选项 | +| WasmEdge 解释器 | 374 | | + +- **原生动态库**没有隔离;Theseus OS 的模型(受信任的编译器、单一地址空间)不适用于手机 + 操作系统。 +- **上游 Makepad 的 JIT** 编译的是 Splash,不是 Rust。 +- **Splash 计算内核**仍然是数值内核的答案。 + +## 测量(Wasm Lab,`apps/wasmlab`) + +只有轻量算法:Markdown 转 HTML(pulldown-cmark)、在忙碌时段之间找空闲时段、模糊排序 +(strsim)、按行差异比较(similar)。 + +200 次调用的中位数,release 构建,桌面运行三次、手机运行三次 +(`cargo run --release -p octosense-wasm-host --example measure`;手机上是同一个示例, +交叉编译后通过 adb 运行): + +| | 桌面(M5 Max) | 手机(Redmi Note 12,骁龙 685) | +| --- | --- | --- | +| 模块 | 433 KiB | 433 KiB | +| 编译(首次加载) | 27–33 毫秒 | 378–421 毫秒 | +| 从缓存加载 | 新进程中 1.0–1.4 毫秒;刚写入后 4.8–6.4 毫秒 | 5.0–7.0 毫秒 | +| 实例化 | 0.07 毫秒 | 0.19–0.27 毫秒 | +| md_to_html,32 KiB | 405–412 微秒(原生的 2.1–2.2 倍) | 2,325–2,328 微秒(1.7 倍) | +| find_slots,30 个忙碌时段 | 6.7 微秒(1.7 倍) | 40 微秒(1.5 倍) | +| fuzzy_rank,500 项 | 138–146 微秒(1.6–1.7 倍) | 841–845 微秒(1.4 倍) | +| text_diff,1000 行 | 231–254 微秒(1.6–1.7 倍) | 1,434 微秒(1.9 倍) | +| 最小调用(双向 JSON) | 0.3 微秒 | 2.5–2.6 微秒 | + +用 wasm SIMD(`+simd128`)构建客体没有缩小 Markdown 的差距(2.2–2.3 倍):差距来自 +Cranelift 与 LLVM 的代码生成,而不是向量化。Wasmtime 和 Cranelift 给 Android 库增加约 +7.5 MiB 代码(按符号大小;桌面上测得 7.4 MiB)。 + +在外壳中,脚本用 `host.request` 调用自己的函数并收到结果,桌面上约 2 毫秒,手机上 +1.8–2 毫秒,几乎全部花在 UI 事件循环上;函数本身在桌面上用 18–72 微秒,在手机上用 +47–190 微秒。在手机的 Home 中,模块首次启动时编译用了 670 毫秒(应用当时还在启动),之后 +从缓存加载用 7.4–11 毫秒。四种失控模式(死循环、无限分配、panic、无限递归)分别在 +2,000 毫秒、140 毫秒、2.2 毫秒和 3 毫秒后以错误结束,Home 一直在运行。 + +两个平台发现: + +- **Android 上的陷阱需要 libc 0.2.190。** 使用 libc 0.2.189 时,Wasmtime 通过信号捕获的 + 每一种陷阱(panic 的 `unreachable`、越界读取、栈溢出)都会让 Home 退出,普通的命令行 + 程序也一样。Wasmtime 的信号处理函数确实运行了,但它通过 libc 的 `ucontext_t` 读取出错 + 位置的 pc,而 aarch64 Android 的这个结构缺少内核在信号掩码后面加的填充,所以没有一个 + 陷阱被认出是 wasm。0.2.190 修正了这个布局;`crates/wasm-host` 现在要求这个版本。显式陷阱 + (`signals_based_traps(false)`)也能工作,但每次调用慢 19–38%,编译时间翻倍。 +- **桌面上的扫描程序会让缓存比编译还慢。** 在受管理的 Mac 上,Microsoft Defender 会把新写入 + 的缓存文件的第一次打开挡住约一秒;外壳的新构建第一次打开它时又会被挡一次(1,086 毫秒; + 下次启动 2.3 毫秒)。写入方会在后台把文件读一遍,但只能覆盖第一种情况。那里编译只要 + 30–60 毫秒,所以桌面可以不用缓存,或让两者竞速;在手机上,缓存能省下约 0.4 秒。 + +## 影响 + +- 对这些工作负载,第三方 Rust 在 OctoSense 应用中的运行时间是原生的 1.4–2.2 倍,不引入 + 新的信任,默认构建不变。 +- Android APK 增加 3.5 MiB(从 209.1 MB 到 212.8 MB,含 Wasm Lab 自己的应用包);运行时 + 约为 7.5 MiB 代码。 +- 在中端手机上,每个模块编译约 0.4 秒(Home 启动期间 0.67 秒)。有了缓存,之后的加载只要 + 5–11 毫秒;在安装时编译(商店应用)或在构建时编译(系统应用)可以省掉第一次。 +- 在 App Hub #140 的商店检查下,商店应用包可以携带 `fns/*.wasm`(核心模块,最多 8 个, + 只能配合 `wasm` 能力)。到目前为止,只有系统应用在设备上运行过函数。 +- iOS 不允许应用使用 JIT,也不允许下载原生代码:那里只能用 Pulley(比 Cranelift 慢约 + 17 倍)。随系统发布的应用,其函数可以在构建应用时转成原生代码;商店应用不行。 + OpenHarmony 的 JIT 策略未知(**未验证**)。 +- 限制是按调用、按实例计算的。目前还没有办法阻止一个应用用连续调用占满一个核心,或在 + 它的八个模块里各用 256 MiB;按应用的 CPU 和内存预算是后续工作。App Hub 已判定超时的 + 请求,仍会在工作线程里运行。 +- 外壳运行期间更新的应用,在外壳重启前仍使用旧函数;工作线程与外壳同寿命。 + +## 未决问题 + +- 用带类型的接口(组件模型和 WIT)代替 JSON 输入输出。 +- `octo.log` 之外的任何宿主导入(时钟、随机数)都应是新的能力,而不是默认提供。 +- epoch 截止时间开销小但不确定;fuel 是确定的,但有代价。 diff --git a/docs/adr/README.md b/docs/adr/README.md index 2d1e20aa..d72d6b9b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -17,6 +17,7 @@ How these decisions fit together in the code on `main`, and which parts are stil | [0007](0007-composable-mail-action-cards.md) | Composable Mail cards with editing, chat and approved actions | Implementation in progress; phone acceptance pending | | [0008](0008-quiet-android-mail-jobs.md) | Quiet Android Mail jobs and native card notifications | Implemented in this change; device acceptance in progress | | [0010](0010-shared-oauth-and-connected-apps.md) | Shared OAuth and independently installed connected apps | Implementation in progress; live sign-in passed on macOS; GitHub writes, Gmail sends and device acceptance pending | +| [0011](0011-apps-own-functions-in-webassembly.md) | An app's own functions, in WebAssembly | Proposed; behind the `wasm-lab` feature, validated on macOS and an Android phone | | [0012](0012-app-host-api-discovery.md) | Discoverable host APIs, signed backend operations and live script tools | Implemented in source; contract 1.6.0 published; host release and phone acceptance pending | ## Home (phone shell) decisions, 2026-09-16 to 2026-09-25 diff --git a/docs/adr/README.zh-CN.md b/docs/adr/README.zh-CN.md index c5c79790..34478caf 100644 --- a/docs/adr/README.zh-CN.md +++ b/docs/adr/README.zh-CN.md @@ -17,6 +17,7 @@ OctoSense 仓库的决策:Shell、Shell 服务、系统应用,以及桌面 | [0007](0007-composable-mail-action-cards.zh-CN.md) | 可组合的 Mail 卡片:编辑、聊天与经批准的操作 | 实现中;手机验收待完成 | | [0008](0008-quiet-android-mail-jobs.zh-CN.md) | Android 静默邮件后台任务与原生卡片通知 | 本次变更已实现;设备验收进行中 | | [0010](0010-shared-oauth-and-connected-apps.zh-CN.md) | 共享 OAuth 与独立安装的连接账户应用 | 实现中;真实账户登录已在 macOS 上通过;GitHub 写入、Gmail 发信和设备验收待完成 | +| [0011](0011-apps-own-functions-in-webassembly.zh-CN.md) | 应用自带的 WebAssembly 函数 | 提议中;在 `wasm-lab` 特性之后,已在 macOS 和一部 Android 手机上验证 | | [0012](0012-app-host-api-discovery.zh-CN.md) | 可发现宿主 API、签名后端操作与活动脚本工具 | 源码已实现;契约 1.6.0 已发布;宿主发布和手机验收待完成 | ## Home(手机 Shell)的决策,2026-09-16 至 2026-09-25