docs: browser runtime design (craft-libs, workers, #1221) (#1258)

A design note for SolidCraft in the browser: tiered storage, a SharedWorker job broker, GPU dispatch and in-browser agent control through the same engine commands, with the generic pieces in craft-libs. Linked from the MCP and web getting-started docs.
This commit is contained in:
William Dunn authored and GitHub committed 2026-10-14 22:34:48 -04:00
1 parent 784648af2a
commit 41f0c3d0c6
4 files changed
+310 -1

No files matched your search

+3 -1
View File
@@ -74,7 +74,9 @@ doesn't update automatically.
The same app runs in the browser: [open SolidCraft on the web](../app/) (`?sample` opens the sample
part, `?webgl` forces WebGL2 where WebGPU is missing). Designs are saved in the browser's private
file storage, which needs HTTPS or localhost.
file storage, which needs HTTPS or localhost. Planned worker and tiered-storage integration is
described in [web-runtime.md](https://github.com/bherbruck/solidcraft/blob/main/docs/web-runtime.md)
(design; not yet wired in the shipped wasm build).
After the first visit the web app works offline: the browser keeps a copy of it. Online, each
launch gets the latest published build (the browser downloads it once when it changes); offline,
+5
View File
@@ -10,6 +10,11 @@ ordinary `execute` tool: `render.preflight`, `render.start`, `render.status`,
`render.cancel` and `render.export`. Keep the complete returned job ID; image bytes remain
in the session until explicit PNG export. See [render jobs](render-jobs.md).
For how the same command surface should work in the browser (workers, tiered storage, in-tab
agents), see [web-runtime.md](web-runtime.md). For sandboxed plugins and parametric timeline
features that also use `run(command, args)`, see [plugins.md](plugins.md). Desktop MCP in this
document remains the reference implementation today.
## Start it
```sh
+146
View File
@@ -0,0 +1,146 @@
# Plugins (design)
Design companion to [#1221](https://github.com/bherbruck/solidcraft/issues/1221): sandboxed
WebAssembly plugins with a WIT interface, the same `run(command, args)` JSON as
[MCP](mcp.md) and the [control protocol](control-protocol.md). Browser hosting is in
[web-runtime.md](web-runtime.md).
This document adds **parametric timeline features**: a generic host-owned feature shell that
plugins fill with parameters, display metadata, and rebuild logic, without a second timeline
model or a private geometry API.
## Goals (unchanged from #1221)
- Sandboxed by default: manifest permissions, fuel/memory limits on desktop, broker enforcement on web.
- Any language that compiles to Wasm components; Rust first.
- **One command surface:** plugins change the model only through engine commands (undoable, journaled).
- Versioned WIT world (`solidcraft:plugin@1.0.0`).
## Two plugin shapes
| Shape | Timeline | Rebuild | Milestone |
|-------|----------|---------|-----------|
| **One-shot** | Optional; often no persistent feature | N/A | 1 (first ship) |
| **Parametric** | One **`Plugin` feature row** per plugin instance | Host calls plugin on param/timeline rebuild | 2 |
One-shot plugins call `execute(params)` once; they may emit ordinary native features (sketch,
extrude, …) inside that transaction. Parametric plugins still use `run()` inside rebuild; the
difference is that the document **records** a single feature whose params the user edits later.
## Generic feature shell (host-owned)
Add one variant to the parametric document (name illustrative):
```text
FeatureKind::Plugin {
plugin_id: String, // "gear_generator"
api_version: u32, // matches WIT world
params: Value, // JSON owned by the plugin schema
display: PluginDisplay, // timeline + browser presentation
provenance: PluginProvenance, // optional: manifest hash, build id
}
```
**`PluginDisplay`** (stored in the file, not recomputed from plugin code at open):
| Field | Role |
|-------|------|
| `label` | Primary timeline / browser text |
| `icon_id` | Host icon registry name (same system as [`feature_icon`](https://github.com/bherbruck/solidcraft/blob/main/crates/ui-egui/src/workspace.rs)) |
| `tooltip` | Hover text, `inspect` type string |
| `type_name` | Fusion-like feature type for power users |
The timeline renders **any** `Plugin` feature through one code path: read `display`, draw icon +
label, wire suppress/rename/delete/reorder like built-in features. No per-plugin egui in milestone 2.
Optional later: **`parameter_schema()`** in WIT returns `{ id, label, kind, min, max, … }` so the
host renders sliders and dimension rows without custom UI per plugin.
## WIT entry points (phase 2 additions)
Milestone 1 (#1221): `manifest()`, `execute(params) -> result<string, string>`.
Milestone 2 parametric:
| Export | Purpose |
|--------|---------|
| `evaluate(params, ctx) -> result<string, string>` | Rebuild: plugin runs `run()` commands; result is success JSON or error |
| `parameter_schema() -> string` | Optional JSON schema for generic parameter UI |
| `fallback_policy() -> string` | Declares missing-plugin behaviour (see below) |
`ctx` is host-provided read-only hints (configuration name, units, feature id); plugins must not
rely on it for correctness.
Inside **`evaluate`**, the plugin uses the same **`run(command, args)`** import as one-shot mode.
The host applies commands to the current eval context (or a scratch session merged by the host).
No second registry of geometry ops.
## Rebuild flow
```text
Timeline rebuild reaches FeatureKind::Plugin
-> load plugin (wasmtime desktop / broker web) if permitted + present
-> evaluate(stored params)
-> plugin calls run("sketch.line", …), run("solid.extrude", …), …
-> host merges geometry like any native feature
-> errors surface on the feature row like native eval errors
```
Param changes (parameter panel, `parameters.change`, config switch) re-run **`evaluate`** for
affected plugin features only when dependencies say so (same invalidation rules as native features).
## Missing plugin and API drift
Manifest declares **`fallback_policy`**:
| Policy | Behaviour |
|--------|-----------|
| `recorded_mesh` | Optional B-rep or mesh blob stored beside params; open works, edit disabled without plugin |
| `recorded_script` | Stored list of native commands; rebuild without plugin if every command is built-in |
| `suppress_with_warning` | Feature suppressed; user sees clear message |
| `error_on_rebuild` | Hard fail on eval (strict teams) |
Design files shared in CollabCraft should prefer a policy that **opens safely** (`recorded_mesh` or
`recorded_script`). CI for plugins (#1221) should include: uninstall plugin, reopen file, assert
document still opens and policy matches manifest.
## Icons and metadata
Plugins declare allowed **`icon_id`** values in `manifest()`; the host rejects unknown ids. Icons
stay in the app registry so untrusted plugins cannot inject arbitrary raster assets into the tree
without a reviewed icon pack.
**Rename** by the user updates the feature user label; **`display.label`** remains the default from
creation unless the plugin updates it via a host callback (optional future).
Command naming: prefer **`plugin_id.function`** in `run()` or registered command aliases to avoid
collisions ([#1221 discussion](https://github.com/bherbruck/solidcraft/issues/1221)).
## Group header variant (lighter-weight v2a)
Before full **`evaluate`**, a stepping stone: one-shot plugin creates a **feature folder** whose
header carries `PluginDisplay` metadata while children are ordinary native features. Rebuild does
not need the plugin until the user edits plugin-owned params on the header. Migrates cleanly to
full `FeatureKind::Plugin` when `evaluate` lands.
## Relationship to craft-libs
Plugin **document model and eval** stay in SolidCraft (`crates/doc`, `crates/engine`). craft-libs
provides worker/broker hosting only ([web-runtime.md](web-runtime.md)). Neutral serde types
(`VersionedParams`, `PluginDisplay`) may move to a shared crate later if multiple apps need them;
CAD plugins remain SolidCraft-specific.
## Phasing
1. WIT + wasmtime host + one-shot `execute` + permissions tests (#1221 milestone 1).
2. `FeatureKind::Plugin` + `PluginDisplay` + timeline generic row + `evaluate` + fallback policy.
3. Web broker runs same WIT; parametric features offline only if plugin module cached and policy
allows.
4. Parameter schema UI, cloud-signed plugin index, inspector extensions.
## Tests (parametric)
- Create plugin feature, change param, rebuild, measure geometry (oracle or bbox).
- Undo/redo across plugin eval.
- Remove plugin binary, reopen design, assert fallback policy.
- Icon id from manifest appears on timeline (scenario or screenshot harness).
+156
View File
@@ -0,0 +1,156 @@
# Browser runtime (design)
Design note for SolidCraft in the browser: tiered storage, worker offload, GPU jobs, and
agent control that stay aligned with the desktop app, the control channel, and
[MCP](mcp.md). Implementation lives primarily in
[craft-libs](https://github.com/storytold/craft-libs) (`craft-tier-store`, `craft-worker-host`,
`craft-gpu`, `craft-runtime`); SolidCraft wires those crates into
[`apps/solidcraft-web`](../apps/solidcraft-web) and [`files.rs`](../apps/solidcraft-web/src/files.rs)
rather than growing a second stack here.
Related: [plugins.md](plugins.md) and [#1221](https://github.com/bherbruck/solidcraft/issues/1221)
(desktop WIT + wasmtime first; parametric timeline features in phase 2), [design containers](design-container.md),
[control protocol](control-protocol.md).
## Problem
The wasm app today ([`getting-started`](../docs/book/src/guide/getting-started.md)) keeps designs in
the origin private file system (OPFS) behind virtual `/opfs/` paths, with optional File System
Access import/export. That works for small and medium designs but hits three walls:
| Wall | Symptom in SolidCraft |
|------|------------------------|
| Quota | Large STEP imports and container blobs compete with autosave and cache in OPFS |
| Main thread | Open, save, and timeline evaluation run synchronously on the UI thread with egui |
| Agents | MCP runs out of process (`solidcraft-cli mcp`); the tab has no first-class agent session |
WebGPU for the viewport already runs on the main thread (wgpu via eframe). Moving the entire UI
renderer into a worker is out of scope for the first integration; worker GPU is for offload jobs
(tessellation warm paths, future kernel work), not for egui.
## Principles
1. **One command surface.** Toolbar, scripts, control channel, MCP, and (eventually) in-browser
agents all use the same engine commands. Dialog-free commands only for unattended paths, same
rule as [MCP](mcp.md).
2. **Generic traits in craft-libs.** Storage tiers, worker RPC, and GPU dispatch are implemented
once in craft-libs. Web and native use the same traits; hosts differ (OPFS vs paths, SharedWorker
vs thread pool).
3. **Do not duplicate cloud IO.** Authenticated cloud Open/Save uses `craft-cloud` and CollabCraft
([craft-libs #48](https://github.com/storytold/craft-libs/pull/48)). Browser-local tiers
(`craft-tier-store`) are separate: hot mirror, warm cache, cold working folder, optional range
reads of cloud blobs through CollabCraft without vendor types in generic crates.
4. **One worker broker.** A SharedWorker (or dedicated-worker fallback) coordinates storage writes,
job queues, and shared wasm module compile per origin. Plugins and offload jobs register on the
broker; do not spawn one SharedWorker per plugin ([#1221 comment thread](https://github.com/bherbruck/solidcraft/issues/1221)).
## Architecture
```text
Main thread (tab)
eframe + viewport WebGPU
hot mirror (sync reads for engine)
craft_runtime HostFacade ----MessagePort RPC----> SharedWorker broker
tier-store coordinator (single writer)
job queue + dedup
headless engine session(s)
optional dedicated GPU workers
ProviderRegistry (GpuJob dispatch)
Tiers (craft-tier-store)
Hot RAM mirror, LRU budget
Warm OPFS under craft/<appId>/… (today: /opfs/ mapping)
Cold File System Access project folder (no duplicate bytes in OPFS)
Cloud craft-cloud refs, range fetch via CollabCraft API (not in wasm credentials)
```
On **desktop**, the same `BlobStore` and job envelopes apply: warm/cold map to normal directories;
[`solidcraft-cli mcp`](mcp.md) and wasmtime plugin hosts ([#1221](https://github.com/bherbruck/solidcraft/issues/1221))
call the same `run(command, args)` JSON.
## Mapping SolidCraft today
| Today | Target |
|-------|--------|
| [`files.rs`](../apps/solidcraft-web/src/files.rs) OPFS + `/opfs/` VFS | Warm tier keys; migration keeps existing filenames initially |
| Recovery autosave | Warm tier, same semantics as [`compatibility.md`](compatibility.md) |
| [Design containers](design-container.md) content-addressed blobs | Tier keys by hash; cold tier holds large blobs without copying into OPFS |
| [Control protocol](control-protocol.md) over TCP | Same methods over `MessagePort` to broker headless session (WebMCP-shaped, not a second registry) |
| Parallel mesh work ([ROADMAP](../ROADMAP.md): web path serial) | Offload eval / IO jobs via `JobSubmit`; viewport stays on main thread |
## craft-libs crates (reference)
These names follow the craft-libs performance platform design; SolidCraft depends on them as
published from storytold/craft-libs after cloud contracts land (#48, #52).
| Crate | Role |
|-------|------|
| `craft-tier-store` | `BlobStore`, tier policy, sync mirror over async persist; native + wasm hosts |
| `craft-worker-host` | SharedWorker bootstrap, RPC messages (`Init`, `StorageOp`, `JobSubmit`, …) |
| `craft-gpu` | `GpuContext`, `GpuJob`, `GpuProvider`, `ProviderRegistry` (no app WGSL here) |
| `craft-gpu-wgpu` | wgpu backend: WebGPU on wasm32, Vulkan/DX12/Metal on desktop |
| `craft-gpu-providers` | Shared readback/composite helpers extracted from Craft app gpu crates |
| `craft-runtime` | Facade: tier store + worker host + optional `cloud-delegate` hooks |
Generic runtime code must not depend on SolidCraft crates. SolidCraft registers app-specific
providers and key namespaces only.
## GPU and the viewport
- **Main thread:** eframe owns the WebGPU device used for the 3D view and UI.
- **Workers:** `craft_runtime::gpu::dispatch(registry, jobs)` runs `GpuJob` lists (chunked buffers,
readback fences) for offload; same entry point on native thread pools.
- **Limits:** Capability probe once at init; UI and workers share reported max buffer sizes.
## Agents in the browser
| Mode | Transport |
|------|-----------|
| Desktop agent | `solidcraft-cli mcp` (stdio) or `--connect` to [control protocol](control-protocol.md) |
| Browser agent | RPC to broker session; dialog-free tools only |
| IDE bridge | Optional local relay (stdio MCP ↔ tab); not required for CollabCraft-hosted wasm |
File policy for web agents mirrors MCP folders: design directory, cold tier roots, recovery;
never `--allow-any-path` in the tab.
## Plugins ([#1221](https://github.com/bherbruck/solidcraft/issues/1221))
| Host | Runtime |
|------|---------|
| Desktop | wasmtime + fuel/memory limits; WIT `solidcraft:plugin@1.0.0` with `run`, `manifest`, permissions |
| Web | Component or module in broker; same WIT contract; sandbox = manifest permissions enforced before `run()` |
Milestone 1: one-shot commands and importers/exporters. Milestone 2: **parametric timeline
features** via a generic host-owned `FeatureKind::Plugin` shell (params + display metadata
including timeline icon and label); plugins rebuild through WIT `evaluate()` calling the same
`run()` JSON. See [plugins.md](plugins.md). Command ids may use a `pluginname.function` prefix to
avoid collisions.
## CollabCraft deployment
Static wasm apps on `craft.quasarke.net` ship `craft-worker-host.js` (brotli-precompressed) beside
the app bundle. Prefer one global runtime worker per origin once message routing and wasm size are
budgeted; per-app worker URLs are acceptable for pilots. CSP must allow `worker-src` / SharedWorker.
## Phasing (SolidCraft)
1. **Document and track** (this file); no user-visible change.
2. **Tier-store wiring:** replace ad-hoc OPFS glue with `craft-tier-store` keys; keep on-disk layout.
3. **Worker broker:** headless `engine.execute` batches and container IO off UI thread.
4. **Agent bridge:** MessagePort tool surface equivalent to MCP dialog-free tools.
5. **Plugins on web:** after desktop wasmtime milestone and WIT stability.
6. **Parametric plugin features:** [plugins.md](plugins.md) phase 2 (`evaluate`, display metadata, fallback policy).
## Out of scope (initial integration)
- Moving egui/viewport WebGPU into SharedWorker
- Second non-wgpu GPU backend in craft-libs
- **Implementing** parametric `FeatureKind::Plugin` (designed in [plugins.md](plugins.md); follows craft-runtime wiring)
- Duplicating `craft-cloud` or embedding `craft-nextcloud` in SolidCraft wasm
## Tests
- Extend control-channel scenarios for broker RPC parity where methods map 1:1
- Worker tests: SharedWorker multi-tab dedup (EffectCraft `workers.mjs` pattern)
- Tier-store: native LRU tests + wasm OPFS smoke
- GPU: golden frames remain in app crates; craft-gpu-null for CI dispatch smoke