docs(research): extend alknet-desktop POC summary — operations protocol verified on quickjs
The quickjs-reactive-probe was extended to load @alkdev/operations (registry, call protocol, response envelopes, ACL, buildCallHandler) alongside the reactive core. All five operations assertions pass on QuickJS-NG via rquickjs: registry/execute/envelope/acl/callHandler. 271 modules loaded total. This closes the third highest-leverage unknown: the operations protocol is runtime-agnostic in practice, not just in theory. Adds a new section on the QuickJS UDF host convergence — a minimal isolate speaking the same bidirectional operations protocol as the TypeScript reference, the Rust alknet-call port, and the planned NAPI/Python adapters, without needing Node/Deno/Bun. Connects to the toolEnv WASM-QuickJS sandbox precedent at /workspace/toolEnv.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# alknet-desktop: POC Research Summary
|
||||
|
||||
**Status:** Research complete on the two highest-leverage unknowns; further POCs planned before spec.
|
||||
**Status:** Research complete on the three highest-leverage unknowns; further POCs planned before spec.
|
||||
**Date:** 2026-06-20
|
||||
**Scope:** Captures what the two completed POCs proved, what unknowns they closed, what remains open, and the architectural direction they jointly establish. Source material for the eventual `alknet-desktop` crate spec.
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Two POCs were completed that, between them, resolve the two largest sources of feasibility uncertainty around building `alknet-desktop` — a custom desktop environment on Rust + wgpu + QuickJS, with ujsx as the user-facing IR over three.js (3D) and an SDF layer (2D UI), networked over alknet's irpc/ALPN infrastructure.
|
||||
Two POCs were completed that, between them, resolve the three largest sources of feasibility uncertainty around building `alknet-desktop` — a custom desktop environment on Rust + wgpu + QuickJS, with ujsx as the user-facing IR over three.js (3D) and an SDF layer (2D UI), networked over alknet's irpc/ALPN infrastructure.
|
||||
|
||||
1. **`ui-spoke-poc`** (`/workspace/ui-spoke-poc`) — proved that headless WebGPU rendering in Deno works end-to-end, including the hard cases: driving three.js's `WebGPURenderer` without a DOM, and MSDF text rendering (the "2D UI is rocket surgery" subproblem). Established the `HeadlessCanvas` abstraction and the device-capture technique that makes it work.
|
||||
2. **`quickjs-reactive-probe`** (`/workspace/quickjs-reactive-probe`) — proved that the reactive core (`@preact/signals-core`, `@alkdev/typebox` Value namespace, and `@alkdev/ujsx`'s fiber-based reconciler with full mount/update/unmount) runs cleanly on QuickJS-NG via rquickjs. The runtime-compat question that could have sunk the whole approach is closed: it works.
|
||||
2. **`quickjs-reactive-probe`** (`/workspace/quickjs-reactive-probe`) — proved that three QuickJS-NG runtime-compat questions are all non-issues:
|
||||
- The **reactive core** (`@preact/signals-core`, `@alkdev/typebox` Value namespace, `@alkdev/ujsx`'s fiber-based reconciler with full mount/update/unmount) runs cleanly via rquickjs.
|
||||
- The **operations protocol** (`@alkdev/operations` registry, call protocol, response envelopes, ACL enforcement, `buildCallHandler` bidirectional surface) also runs cleanly — the same probe was extended to register UDFs, execute them, verify envelope schema validation, and confirm ACL blocks missing scopes.
|
||||
- This closes the runtime-compat question for both the desktop's reactive UI layer *and* the broader "QuickJS as a first-class UDF host" direction (see §QuickJS UDF Host Convergence below).
|
||||
|
||||
Both POCs were run on this OVH box, which has no physical GPU. wgpu renders against Mesa's `llvmpipe` (LLVM 20.1.2, 256-bit SIMD) software Vulkan driver — a property that reinforces the design rather than compromising it (see §Headless/Headed Parity below).
|
||||
|
||||
@@ -74,11 +77,11 @@ The MSDF POC is the proof that 2D UI on the same wgpu surface as 3D is not a res
|
||||
|
||||
---
|
||||
|
||||
## POC 2: `quickjs-reactive-probe` — ujsx Reconciler on QuickJS-NG
|
||||
## POC 2: `quickjs-reactive-probe` — ujsx Reconciler + Operations Protocol on QuickJS-NG
|
||||
|
||||
**Location:** `/workspace/quickjs-reactive-probe`
|
||||
**Runtime:** rquickjs 0.12 wrapping QuickJS-NG (ES2020)
|
||||
**Status:** Probe passes; reactive core verified compatible.
|
||||
**Status:** Probe passes; reactive core and operations protocol both verified compatible.
|
||||
|
||||
### The question
|
||||
|
||||
@@ -89,34 +92,44 @@ The MSDF POC is the proof that 2D UI on the same wgpu surface as 3D is not a res
|
||||
- Signal wiring via `@preact/signals-core`'s `effect()` (`reconcile.ts:227-238`)
|
||||
- Microtask-batched updates via `queueMicrotask` (`reconcile.ts:45-79`)
|
||||
|
||||
QuickJS-NG targets ES2020. The POC code itself is ES2020-or-earlier (optional chaining, nullish coalescing, public class fields, TLA), but TypeGPU and three.js's WebGPU build ship as TS that may emit ES2022+ patterns (class static blocks, `Array.prototype.at()`, top-level `await` in modules). The probe's job was to determine whether the reactive core — the part we own — runs on QuickJS-NG before we commit to the runtime. three.js compat is a separate, scoping question (see §Open Unknowns).
|
||||
A second, related question: does `@alkdev/operations` (the bidirectional operations registry + call protocol that alknet-call is being specced from) also run on QuickJS-NG? The JS side in a quickjs UDF host is intentionally thin — registry + call + types + validation + response-envelope. Heavy adapters (MCP, HTTP, OpenAPI) live at the Rust layer. But the core protocol uses `EventTarget`-based `PendingRequestMap`, `@alkdev/pubsub`'s `Repeater` (async iterators via `Symbol.asyncIterator`), and `@logtape/logtape` (which uses Node-style `#util` subpath import conditions). All three needed verification.
|
||||
|
||||
QuickJS-NG targets ES2020. The POC code itself is ES2020-or-earlier, but TypeGPU and three.js's WebGPU build ship as TS that may emit ES2022+ patterns. The probe's job was to determine whether the parts we own — the reactive core and the operations protocol — run on QuickJS-NG before we commit to the runtime. three.js compat is a separate, scoping question (see §Open Unknowns).
|
||||
|
||||
### What the probe exercises
|
||||
|
||||
The probe (`/workspace/quickjs-reactive-probe/probe.mjs`) loads the *built* ESM distributions of all three libraries:
|
||||
The probe (`/workspace/quickjs-reactive-probe/probe.mjs`) loads the *built* ESM distributions of all relevant libraries:
|
||||
|
||||
- `@preact/signals-core` — `signals-core.module.js` (minified, ES5-ish prototype style; references `Symbol.dispose` only as a property assignment, not via `using`, so it won't crash even if the symbol is absent)
|
||||
- `@alkdev/typebox` + `@alkdev/typebox/value` — the ESM build at `build/esm/` (250 modules loaded transitively)
|
||||
- `@alkdev/ujsx` — the bundled chunks at `dist/` (host/config → `chunk-UBTVTQ75.js` for the reconciler, `chunk-NGTIHDKG.js` for `h`/`createComponent`, etc.)
|
||||
- `@alkdev/operations` — `dist/index.js` (pulls in `chunk-GGC64AOH.js` for core types/envelopes, `chunk-BWLYSLNL.js` for MCP adapters with *dynamic* `await import("@modelcontextprotocol/sdk")` so the heavy SDK never loads unless `createMCPClient` is called, `chunk-CXAK3FQT.js` for schema adapters)
|
||||
- `@alkdev/pubsub` — `dist/index.js` (the `Repeater` class with `Symbol.asyncIterator` support)
|
||||
- `@logtape/logtape` — `dist/mod.js` with the `#util` subpath import mapped to the neutral `dist/util.js` build (pure JS, `JSON.stringify`-based `inspect`)
|
||||
|
||||
It then runs four assertions groups:
|
||||
It then runs five assertion groups:
|
||||
|
||||
1. **signals-core**: `signal(0)` → `effect()` observes initial value → `s.value = 42` propagates → `batch()` + `computed()` recompute correctly.
|
||||
2. **typebox Value namespace**: `Type.Object(...)` builder works → `Value.Hash()` returns `bigint` → `Value.Diff()` returns array → `Value.Clone()` deep-copies → `Value.Equal()` compares.
|
||||
3. **ujsx reconciler mount/update/unmount**: build `<div count={0}><span>hi</span></div>`, render via a no-op `HostConfig` that records every call, verify `createInstance("div")`/`createInstance("span")`/`createTextInstance("hi")` fire; update to `<div count={5}><span>bye</span>`, verify `commitUpdate` fires for the changed `count` prop; unmount, verify `finalizeRoot` fires.
|
||||
4. **Reactive wiring**: a `createComponent` that reads a `signal()` in its render fn mounts successfully via `createRoot(dynHost, {}).render(h(Counter, {id: "c1"}))`.
|
||||
5. **Operations protocol**: register a QUERY UDF (`test.echo`) and a MUTATION UDF (`counter.increment`) with TypeBox schemas and `accessControl.requiredScopes`; execute with valid identity → returns `ResponseEnvelope` with handler output; `isResponseEnvelope()` validates; `Value.Check(ResponseEnvelopeSchema, envelope)` passes schema validation; `unwrap()` extracts data; `localEnvelope()` constructs correctly; ACL blocks missing scope (throws), allows correct scope; `buildCallHandler({registry, respond, emitError})` returns a function — the bidirectional call surface is operational.
|
||||
|
||||
### Result
|
||||
|
||||
```
|
||||
EVAL_OK result={"signals":"ok","typebox":"ok","reconcilerMount":"ok",
|
||||
"reconcilerUpdate":"ok","reconcilerUnmount":"ok",
|
||||
"reactiveWiring":"ok","totalHostCalls":2,"callsBeforeSignal":2}
|
||||
total modules: 253
|
||||
"reactiveWiring":"ok",
|
||||
"operationsRegistry":"ok","operationsExecute":"ok",
|
||||
"operationsEnvelope":"ok","operationsAcl":"ok",
|
||||
"operationsCallHandler":"ok",
|
||||
"totalHostCalls":2,"callsBeforeSignal":2}
|
||||
total modules: 271
|
||||
PROBE PASSED
|
||||
```
|
||||
|
||||
253 modules loaded and linked by QuickJS-NG without a single syntax or import error. Every assertion passed. The reactive core runs on QuickJS-NG via rquickjs.
|
||||
271 modules loaded and linked by QuickJS-NG without a single syntax or import error (the 18-module increase over the reactive-only run is the operations core chunks + logtape + pubsub). Every assertion passed. Both the reactive core *and* the operations protocol run on QuickJS-NG via rquickjs.
|
||||
|
||||
### One observation worth recording
|
||||
|
||||
@@ -124,9 +137,26 @@ PROBE PASSED
|
||||
|
||||
### What it established for alknet-desktop
|
||||
|
||||
- **No runtime-compat blocker at the JS layer.** The three libraries we own (`ujsx`, `typebox`, `signals-core`) all run on QuickJS-NG. The path from "ujsx tree" → "reconciler diff" → "HostConfig.createInstance/commitUpdate calls" is operational.
|
||||
- **No runtime-compat blocker at the JS layer.** The libraries we own (`ujsx`, `typebox`, `signals-core`, `operations`, `pubsub`) all run on QuickJS-NG. The path from "ujsx tree" → "reconciler diff" → "HostConfig.createInstance/commitUpdate calls" is operational, *and* the path from "register UDF" → "execute with ACL" → "return ResponseEnvelope" is operational.
|
||||
- **The HostConfig is the seam.** The probe's no-op `HostConfig` is the shape of the work: a `HostConfig<string, THREE.Object3D, ThreeRootCtx>` for 3D and a `HostConfig<string, SdfInstance, SdfRootCtx>` for 2D UI. Both target the same wgpu surface; the user picks per-subtree via a host boundary. This is the R3F model (React Three Fiber): `<mesh>` becomes `new THREE.Mesh()` inside `createInstance`, prop diffs become three.js mutations inside `commitUpdate`.
|
||||
- **253 modules is the typebox transitive surface.** Worth knowing for cold-start budgeting — alknet-desktop will load this tree once at startup. On quickjs-ng the per-module parse+link cost is low (the whole probe runs in well under a second including compile), but it's not free. A bytecode-bundle preload (rquickjs's `embed!` macro) is the mitigation if cold start matters.
|
||||
- **271 modules is the full transitive surface.** Worth knowing for cold-start budgeting — alknet-desktop will load this tree once at startup. On quickjs-ng the per-module parse+link cost is low (the whole probe runs in well under a second including compile), but it's not free. A bytecode-bundle preload (rquickjs's `embed!` macro) is the mitigation if cold start matters.
|
||||
- **The operations protocol is runtime-agnostic in practice, not just in theory.** `OperationRegistry`, `execute()`, `ResponseEnvelope`, `isResponseEnvelope()`, `unwrap()`, `localEnvelope()`, `Value.Check(ResponseEnvelopeSchema, ...)`, ACL enforcement (`checkAccess`/`enforceAccess`), and `buildCallHandler()` all work. The bidirectional call surface — the proxy shape a quickjs UDF host uses to call ops back into the Rust registry, and to expose its own ops to the Rust side — is operational. This is the seam between the JS UDF layer and the alknet-call Rust layer.
|
||||
|
||||
---
|
||||
|
||||
## QuickJS UDF Host Convergence
|
||||
|
||||
The operations-protocol verification surfaces a broader convergence than just alknet-desktop. The protocol shape — bidirectional operations with TypeBox schemas, ACL metadata, and an event-based call/subscribe protocol — was designed to be agnostic, with multiple hosts speaking it: the TypeScript reference (`@alkdev/operations`, published), the Rust port (`alknet-call`, speccing now), and planned NAPI and Python adapters for authoring UDFs from Node/Python runtimes.
|
||||
|
||||
The probe establishes that a **fifth host — a minimal QuickJS env** — can speak the same protocol without needing a full Node/Deno/Bun runtime. A UDF author writes a plain JS handler, registers it with TypeBox schemas and `accessControl`, and it becomes callable from anywhere on the network. No Node install, no npm tree, no transitive supply-chain surface. Just a ~210 KiB QuickJS isolate + the small set of Rust ops we choose to expose.
|
||||
|
||||
This is directly relevant to alknet-desktop (a worker that renders a window *is* just a UDF host whose operations happen to include "render this ujsx tree" and "handle this input event" — the desktop isn't special, it's an operation host with a GPU), but it also opens:
|
||||
|
||||
- **UDF hosting in untrusted environments.** A remote alknet node accepting UDFs from peers doesn't want Node's surface area. A QuickJS isolate with `allowFetch: false` / `allowFs: false` and an `envProxy` exposing only registered operations is a real sandbox. `toolEnv` (`/workspace/toolEnv`) already proved this concept with a WASM-QuickJS sandbox (`@sebastianwessel/quickjs` + `@jitl/quickjs-ng-wasmfile-release-sync`) for LLM-authored operations; the native rquickjs path is the same privilege model with a different isolation boundary and better perf.
|
||||
- **LLM-authored operations.** An agent composes a tool by emitting JS that calls back into the registry via the `envProxy`. "MCP with scripting capabilities" — MCP gives static tool descriptors; this gives programmable tool composition. `toolEnv`'s `SandboxManager.executeScript(code, env, consoleHandler, timeout)` is the template.
|
||||
- **Edge/embedded deployment.** QuickJS-NG's 210 KiB footprint + rquickjs's no-external-deps build means UDF hosting works where Node can't fit.
|
||||
|
||||
The `toolEnv` first iteration used WASM-QuickJS (browser-compatible, maximal isolation, slower). The alknet-desktop path uses native rquickjs (Rust-embedded, OS-level isolation via process boundaries, faster). Both speak the same operations protocol. The choice between them is a deployment-time decision, not a design-time one — the protocol is the same, the registry is the same, the UDFs are the same.
|
||||
|
||||
---
|
||||
|
||||
@@ -243,21 +273,23 @@ POC 2 noted that `queueMicrotask`-scheduled updates didn't flush before `Module:
|
||||
|
||||
### 6. Things we don't know we don't know
|
||||
|
||||
The two POCs answered the two biggest known-unknowns. The remaining unknowns above are all scope/work questions, not feasibility questions. The SDD (spec-driven development) process — documented at `/workspace/@alkdev/alknet/docs/sdd_process.md` — should not begin until the open unknowns above are resolved by additional small POCs, because a spec written against guesses about scope sizes produces unreliable implementation plans. The recommended next POCs, in priority order:
|
||||
The two POCs answered the three biggest known-unknowns (headless WebGPU rendering, quickjs reactive-core compat, quickjs operations-protocol compat). The remaining unknowns above are all scope/work questions, not feasibility questions. The SDD (spec-driven development) process — documented at `/workspace/@alkdev/alknet/docs/sdd_process.md` — should not begin until the open unknowns above are resolved by additional small POCs, because a spec written against guesses about scope sizes produces unreliable implementation plans. The recommended next POCs, in priority order:
|
||||
|
||||
1. **three.js loader op enumeration** — run GLTFLoader in a quickjs isolate with instrumented globals; produce the concrete op list.
|
||||
2. **Compositing design probe** — render three.js to a texture + SDF layer to a texture + compositor pass onto a wgpu surface, end-to-end, on llvmpipe. Answers the compositing shape question and exercises the v29 surface-from-handle API at the same time.
|
||||
3. **End-to-end skeleton** — `alknet-desktop` crate skeleton: Cargo.toml + lib.rs that opens a winit window, creates a wgpu v29 surface, loads the ujsx reconciler via rquickjs, renders a hardcoded `<div>` tree to the surface via a no-op HostConfig. Proves the full stack integrates before any spec is written.
|
||||
3. **End-to-end skeleton** — `alknet-desktop` crate skeleton: Cargo.toml + lib.rs that opens a winit window, creates a wgpu v29 surface, loads the ujsx reconciler + operations registry via rquickjs, renders a hardcoded `<div>` tree to the surface via a no-op HostConfig, and exposes one UDF ("render") callable from the Rust side. Proves the full stack integrates before any spec is written.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- POC 1: `/workspace/ui-spoke-poc` — `src/headless-canvas.ts`, `src/shims.ts`, `examples/threejs-webgpu.ts`, `examples/msdf-text.ts`, `test/headless_test.ts`
|
||||
- POC 2: `/workspace/quickjs-reactive-probe` — `src/main.rs`, `probe.mjs`, `Cargo.toml`
|
||||
- POC 2: `/workspace/quickjs-reactive-probe` — `src/main.rs`, `probe.mjs`, `Cargo.toml` (extended probe covers reactive core + operations protocol)
|
||||
- Deno desktop source dive (the rejected alternative): `/workspace/conversations/research/deno-desktop-raw-backend-source.md`
|
||||
- SDF/ujsx host design (the 2D UI library this enables): `/workspace/conversations/research/typegpu-sdf-ujsx-webgpu-host.md`
|
||||
- ujsx reconciler source: `/workspace/@alkdev/ujsx/src/host/{reconcile,config,fiber}.ts`
|
||||
- alknet ADRs: `/workspace/@alkdev/alknet/docs/architecture/decisions/` (ADR-005 irpc, ADR-012 stream model, ADR-013 Rust canonical, ADR-017 call client contract — all relevant to the desktop-worker-over-irpc model)
|
||||
- operations protocol source: `/workspace/@alkdev/operations/src/{registry,call,types,validation,response-envelope,access}.ts`
|
||||
- toolEnv (WASM-QuickJS UDF sandbox precedent): `/workspace/toolEnv/core/sandbox/` — `manager.ts` (`SandboxManager`, `SandboxEnv`, `SandboxOptions` with `allowFetch`/`allowFs` privilege flags), `worker/worker.ts` (`@sebastianwessel/quickjs` + `@jitl/quickjs-ng-wasmfile-release-sync`)
|
||||
- alknet ADRs: `/workspace/@alkdev/alknet/docs/architecture/decisions/` (ADR-005 irpc, ADR-012 stream model, ADR-013 Rust canonical, ADR-017 call client contract — all relevant to the desktop-worker-over-irpc model and the UDF-host convergence)
|
||||
- wgpu clone (to be bumped to v29): `/workspace/wgpu` (currently v24.0.5)
|
||||
- llvmpipe ICD: `/usr/share/vulkan/icd.d/lvp_icd.json` (the software Vulkan driver backing all headless rendering on this box)
|
||||
Reference in New Issue
Block a user