Skip to content

Apps' own Rust functions in WebAssembly, behind the wasm-lab feature (ADR 0011) - #364

Merged
ymote merged 2 commits into
mainfrom
feat/wasm-host-mvp
Oct 8, 2026
Merged

ymote merged 2 commits into
mainfrom
feat/wasm-host-mvp

Conversation

@ymote

@ymote ymote commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

An app can ship its developer's own Rust functions, compiled to WebAssembly, and the shell runs them for that app in a sandbox. Everything is behind a new wasm-lab feature, off by default. Default builds are unchanged apart from the App Hub repin and the libc patch bump below. Design and measurements: ADR 0011 (简体中文), proposed.

  • crates/wasm-host: the runtime.
    • Engine: Wasmtime 49, compiled by Cranelift.
    • Limits per call: a 2 s deadline (wall-clock epochs), 256 MiB of memory, 512 KiB of wasm stack, and 16 MiB of input and output.
    • Code cache: keyed by the module's digest and the engine's compatibility hash.
    • Containment: the only import allowed is octo.log. After a trap or a deadline the instance is spent and replaced.
  • The wasm host service (crates/shell/src/wasm_service.rs, feature wasm-lab):
    • Methods: wasm.<function> calls one of the calling app's functions; wasm.functions lists them, with load and per-call numbers.
    • Where modules come from: only the app's own admitted bundle (fns/*.wasm). The service checks the wasm grant itself.
    • Threading: one worker thread per app, never the UI thread.
  • Agent tools: a tool can name host_method: "wasm.<function>" (App Hub ADR 0004: OS contacts as a source for approval rules #140).
  • Wasm Lab (apps/wasmlab), a demo system app:
    • Its functions: fuzzy ranking as you type, free slots, a line diff, Markdown to HTML, and four ways to misbehave.
    • Its agent: three of them as tools.
    • Its guest crate: apps/wasmlab/guest, which builds the module.
    • Where it ships: only in builds that select desktop/ or phone/system-apps-wasm-lab.json.
  • App Hub → 2e576896 (from Implement discoverable host APIs, app backend operations and script tools #360's b1109a39), App Hub main with ADR 0004: OS contacts as a source for approval rules #140 merged. app-contract 1.7.0, with the wasm capability, comes from crates.io; the workspace asks for 1.7.
  • libc 0.2.189 → 0.2.190, also required directly by crates/wasm-host. On aarch64 Android, 0.2.189's ucontext_t lacks the kernel's padding. Wasmtime then misread the faulting pc, and every signal-based trap killed Home.

State

Squashed onto main 4aad89e2 as two commits (code, then the ADR); the earlier history is in this PR's timeline. #360 brought the runtime patch that App Hub's appstore needs (SplashRef::set_device_consent), and #363 brought makepad 32d6415f. This PR moves App Hub from #360's b1109a39 to 2e576896 (App Hub main with #140), and the workspace asks for contract 1.7.

The local CI gate (tools/ci-local.sh --only desktop,phone,apps --linux-host) runs on the exact head; tools/ci-local-merge.sh posts its table here before merging. The ADR stays Proposed until decided.

How it was tested

  • cargo test --locked -p octosense-wasm-host: 18 passed, covering:

    • the real Wasm Lab module against native output;
    • every trap kind;
    • deadlines, memory caps and the code cache.
  • The same 18 tests on a Redmi Note 12 (cross-compiled, run over adb): all passed, but only with libc 0.2.190.

  • Shell tests, against App Hub f5434fa4 (ADR 0004: OS contacts as a source for approval rules #140's head before it merged); on the rebased head, the wasm_service tests pass again, and the full suites run in the local CI gate:

    • cargo test --locked -p octosense-shell --features wasm-lab --lib, with OCTOSENSE_SYSTEM_APPS=desktop/system-apps-wasm-lab.json: 973 passed;
    • from phone/, cargo test --locked --features mobile-apps -p octosense-shell --lib: 988 passed;
    • cargo check --locked -p octosense: builds;
    • tools/setup.py --check --cargo, native_apps.py --check and sync-app-interface.py --check: pass.

    The wasm_service tests run, in a process of their own:

    • a tool call through the shell's executor;
    • the script's requests;
    • each misbehaving mode, followed by a fresh instance;
    • the code cache;
    • an app without the grant;
    • a sheet.
  • Hidden desktop shell (MAKEPAD_HIDE_WINDOWS=1, remote control): Wasm Lab ran end to end:

    • ranking as you type (about 2 ms request round trip);
    • misbehaving modes stopped: a loop at 1.99 s, the memory cap, a panic, recursion.
  • Redmi Note 12, in a separately packaged Home (dev.makepad.octosense.wasmlab; the installed Home was not touched):

    • the module compiled in 670 ms at startup and loaded from the cache in 7.4–11 ms afterwards;
    • script round trip 1.8–2 ms;
    • all four misbehaving modes ended in errors (2,001 ms, 140 ms, 2.2 ms, 3 ms) with Home still running;
    • the APK grows by 3.5 MiB.
  • Speed (--example measure, release):

    • 1.6–2.2× native time on an M5 Max, 1.4–1.9× on the phone;
    • first compile 27–33 ms on the Mac and 378–421 ms on the phone.

Not tested or not done:

  • iOS and OpenHarmony.
  • A live-model run of the agent tools.
  • A store-installed app with functions.
  • Per-app CPU and memory budgets; a request App Hub has timed out still runs.
  • Wasm Lab's phone layout after its last fix (checked on the desktop only).

🤖 Generated with Claude Code

ymote and others added 2 commits October 7, 2026 18:50
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.<function> 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.<function>".
- 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 <noreply@anthropic.com>
The decision record, in English and Chinese: modules in a bundle's fns/,
Wasmtime with Cranelift behind the wasm host service and capability,
wasm.<function> 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 <noreply@anthropic.com>
@ymote

ymote commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Linux evidence for the same head (97fbb1b), from a first tools/ci-local.sh --only desktop,phone,apps --linux-host run, all of whose Linux jobs finished:

[PASS] apps.yml kernel-security: python3 tools/setup.py (0s)
[PASS] apps.yml kernel-security: The pinned octos revision (0s)
[PASS] apps.yml kernel-security: Build octos (unless cached) (0s)
[PASS] apps.yml services: python3 tools/setup.py (0s)
[PASS] apps.yml services: Shared system app interface (0s)
[PASS] apps.yml services: Kernel service (32s)
[PASS] apps.yml kernel-security: Real-kernel tests (46s)
[PASS] apps.yml services: App peers (24s)
[PASS] apps.yml services: AI providers config (5s)
[PASS] apps.yml services: Connected provider protocols and Linux host adapter (43s)
[PASS] apps.yml services: L0 in-card chat (sys.chat, host side) (8s)
[PASS] apps.yml services: System toolbox (workflow templates) (93s)

The extra linux-host / sandbox check failed to compile, independent of this PR: cargo test -p octosense-shell --lib sandbox:: builds the shell without features, and two tests added by #357 (module_resize_tests.rs:525, glance_card.rs:1474) reference app-hub-only items without its cfg gate. This PR touches neither file, and #365 adds the gates. No workflow runs that check, and docs/local-ci.md says a run without it isn't refused, so the merge gate below is a run without --linux-host, where services and kernel-security ran on macOS.

🤖 Generated with Claude Code

@ymote

ymote commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on 97fbb1b (tools/ci-local.sh --only desktop,phone,apps, 34m53s, Darwin arm64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 20s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 7m46s
desktop.yml / desktop One copy of every shell source PASS 0s
desktop.yml / desktop Desktop tools and scripts PASS 36s
desktop.yml / desktop One Makepad, App Hub, octos and Rinx in the locked graph PASS 38s
desktop.yml / desktop The native apps' generated places match native-apps.json PASS 1s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 6s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 2m05s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 4s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 1s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 4s
phone.yml / home Compile Home and its bundled modules PASS 1m14s
phone.yml / home Verify the single runtime graph PASS 4s
phone.yml / home The native apps' generated places match native-apps.json PASS 0s
phone.yml / home The pinned octos revision PASS -
phone.yml / home Build octos (unless cached) PASS -
phone.yml / home Build Home, App Hub admission and runtime policy tests PASS 2m45s
phone.yml / home Test Home, App Hub admission and runtime policy PASS 7m18s
phone.yml / home Two-lane scenario (real kernel, scripted model) PASS 29s
apps.yml / services python3 tools/setup.py PASS 1s
apps.yml / services Shared system app interface PASS 0s
apps.yml / services Kernel service PASS 22s
apps.yml / services App peers PASS 26s
apps.yml / services AI providers config PASS 7s
apps.yml / services Connected provider protocols and Linux host adapter PASS 30s
apps.yml / services L0 in-card chat (sys.chat, host side) PASS 10s
apps.yml / services System toolbox (workflow templates) PASS 2m17s
apps.yml / kernel-security python3 tools/setup.py PASS 1s
apps.yml / kernel-security The pinned octos revision PASS 0s
apps.yml / kernel-security Build octos (unless cached) PASS -
apps.yml / kernel-security Real-kernel tests PASS 30s
apps.yml / apps python3 tools/setup.py PASS 1s
apps.yml / apps Host services (Mail, AI providers, News) PASS 1m23s
apps.yml / apps Connected accounts and Rinx editor PASS 1m28s
apps.yml / apps Shell AI services (crates/ai-host) PASS 2m54s
apps.yml / apps AppCard runtime setup safeguards PASS 0s
apps.yml / apps AppCard clippy (own crates; shared runtime compiles) PASS 52s
apps.yml / apps AppCard transport and store PASS 18s
apps.yml / apps One Makepad, App Hub, octos and Rinx in the locked graph PASS 6s

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • apps.yml:services runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)
  • apps.yml:kernel-security runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)

@ymote
ymote merged commit 72ca95d into main Oct 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant