Adopt llrt runtime modules: fetch, node:fs/os/zlib/path, crypto, subprocess, Web globals#52
Merged
Conversation
Resolver::resolve and Loader::load gained an ImportAttributes parameter; no other API surface we touch changed. All tests pass unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rquickjs 0.12 deprecates the macro in favor of AsyncContext::async_with with stabilized async closures; -D warnings would fail the build. All 27 call sites converted mechanically with move semantics preserved. The llrt_* crates (rev 5af6d48, 2026-06-02) resolve and compile against our feature set; wiring follows in subsequent commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bals Replaces the hand-rolled Web-API layer with AWS's llrt module crates (pinned git rev, all on rquickjs 0.12): - AbortController/AbortSignal are native classes (llrt_abort + DOMException from llrt_exceptions). The Rust Abort handle drives them directly; the JS controller registry and its release() bookkeeping are gone. Plugins gain AbortSignal.any/abort statics and DOMException abort reasons. AbortSignal.timeout is replaced at install with a setTimeout-backed impl: feature unification forces llrt_abort's sleep-timers backend on, and that routes into llrt_timers' process-global runtime table, which is unsound under one runtime per plugin (uninitialised lookup hits unreachable_unchecked; reload reuse aliases freed state). - highbeam:http is gone; the http capability now installs the global fetch (llrt_fetch: Headers/Request/Response/FormData, binary bodies, streams). A guard wrapper injects a 30s default timeout via AbortSignal since llrt_fetch ships none; response size is bounded by the plugin memory cap. - node:fs + node:fs/promises load behind a new coarse `fs` capability (full read/write; the confirm dialog says so explicitly). `fs` also implies the scoped highbeam:fs helpers. node:path is uncapped. - URL/URLSearchParams, Buffer/Blob/File, TextEncoder/TextDecoder (full multi-encoding, replacing the UTF-8-only polyfill), ReadableStream et al are always-on globals via the llrt init fns. - timers stay a local polyfill: llrt_timers' global state model is the same soundness hazard described above. currency-converter and xkcd migrate from http.get to fetch; their vitest suites stub the global. The SDK package drops http.js/http.d.ts and documents that node:* types come from @types/node. New integration suite (tests/runtime_globals.rs) pins the per-capability global surface, node:fs end-to-end read/write, cap gating, and the patched AbortSignal.timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fetch section (with highbeam:http migration notes), node:path and node:fs reference, always-on globals list, new fs capability semantics, internals stack rationale, and cookbook/tutorial/view examples on fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fold a Request instance's embedded signal into the deadline join: llrt gives the init arg precedence, so setting opts.signal silently dropped it. - Reject pre-aborted signals before any I/O. llrt's fetch only subscribes to future abort sends, and AbortSignal.any's already-aborted early-return never sends, so a pre-aborted signal hung the request until the transport gave up instead of rejecting. - Docs: fetch timeout rejections carry TimeoutError, not AbortError; add the TimeoutError row to the error-name table. Fix the Cargo.toml compression comment (zstd stays a C dep under compression-rust). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Abort drops its CancellationToken field and the unused token() / is_aborted() getters: nothing outside the module read them, and the native controller is already idempotent, so cancel() collapses to the single AbortController::abort call. - The http and fs capability checks in install_host_globals go through capability::grants_any, so the fs-implies-scoped-helpers rule reuses the same any_of semantics as the module gate instead of hand-rolled booleans. - Re-indent a test block the mechanical http->fetch swap left misaligned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rquickjs 0.12 bump replaced the async_with! macro with the AsyncContext::async_with method at every call site, but left comment and doc references pointing at the macro form. Surfaced while reconciling the master merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All uncapped (pure compute, no I/O): - crypto global: WebCrypto (getRandomValues/randomUUID/subtle.*) plus node-style randomBytes/createHash/createHmac (crypto-rust backend, no C). - Temporal global: full surface (Now/PlainDate/Instant/...). - Intl global: PARTIAL — DateTimeFormat + supportedValuesOf only, no NumberFormat/Collator (that's all llrt_intl ships). - node:os, node:zlib (compression-rust, C-free), node:string_decoder modules. Intl/Temporal share `jiff` (no heavy ICU). Shape-test pins the new module exports; runtime_globals.rs adds functional probes (crypto UUID/random, Temporal compute, zlib gzip round-trip through Buffer, StringDecoder). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`subprocess` grants both: spawning arbitrary programs (node:child_process, spawn only) and the `process` global, whose `env` is a live proxy over the daemon's real environment (read + write). Confirm wording: "run and spawn arbitrary programs, and read or change the launcher's environment". child_process builds stdio on llrt_stream's Rust constructors directly (no node:stream registration) and uses the Buffer global. process.exit is inert here (sets __exitCode, which the host does not act on) so a plugin can't terminate the daemon through it. Shape-test pins node:child_process exports; runtime_globals.rs adds cap gating for both and a real `/bin/sh -c printf` spawn read back through the stdout stream. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
node:os (exposed in the previous commit) supersedes the custom
highbeam:platform module. Removed src/sdk/platform.rs, its SDK stub and
type declarations, the loader arm, the uncapped-module entry, and the
shape-test pin.
The 6 plugins that used it (app-launcher, kill-process, file-search,
dictionary-linux, prefpanes, window-mgmt) now derive their platform
checks from node:os:
import os from "node:os";
const isMacOS = () => os.platform() === "darwin";
const isLinux = () => os.platform() === "linux";
os.platform() returns Node values ("darwin"/"linux"), not the old
"macos". Their vitest suites mock node:os instead of highbeam:platform.
Docs updated to drop highbeam:platform and point platform detection at
node:os.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the hand-rolled Web-API layer in the plugin runtime with AWS's llrt module crates, pinned to one git rev (
5af6d48, all on rquickjs 0.12). Plugins now get a standard runtime surface instead of high-beam-specific approximations.What plugins see now
fetch(gated on thehttpcapability): realHeaders/Request/Response/FormData, binary bodies,ReadableStreamresponse bodies.highbeam:httpis gone; migration notes live in sdk-reference.md (the gist:get(url, opts)becomesfetch(url, opts)andres.json()is async).node:fs+node:fs/promisesbehind a new coarsefscapability whose confirm-dialog wording says what it means: full read/write access. Declaringfsalso unlocks the scopedhighbeam:fshelpers. The narrowfs.read/fs.cachecaps are unchanged and do not unlocknode:fs.node:path, uncapped.URL/URLSearchParams,Buffer/Blob/File, multi-encodingTextEncoder/TextDecoder(replaces the UTF-8-only polyfill from Fix plugin runtime gaps, enforce host-only actions, trim comment noise #51), web streams,DOMException, and nativeAbortController/AbortSignalwith workingtimeout/any/abortstatics and specDOMExceptionreasons.Host-side changes
ImportAttributesparam; the deprecatedasync_with!macro is replaced byAsyncContext::async_withwith async closures at all 27 call sites).Aborthandle drives the native controller directly; the JS controller registry and itsrelease()bookkeeping are deleted.highbeam:*plus the threenode:*modules; everything else still fails at load with a capability or unknown-module error.Two deliberate divergences from llrt, both soundness-driven
setTimeoutstays our local polyfill.llrt_timerskeys its state in a process-globalVecby raw*mut JSRuntime, never deregisters entries, andunwrap_uncheckeds lookups. Under one runtime per plugin plus reloads that leaks and aliases freed state on address reuse. LLRT itself is single-runtime-per-process and never hits this.AbortSignal.timeoutis replaced at install with a setTimeout-backed implementation. Cargo feature unification forcesllrt_abort'ssleep-timersbackend on (llrt_fetch depends on it with defaults), and the native static routes into the table above: calling it aborted the process withunreachable_uncheckedin testing.Fetch guard
llrt_fetch ships no request timeout, so a wrapper joins every request with a 30 s
AbortSignal.timeoutdeadline (caller signals can abort sooner, not later; response size is bounded by the plugin'smemoryMb). The adversarial review pass surfaced two gaps the guard now covers, with regression tests: aRequest-embedded signal was silently dropped (llrt gives the init arg precedence), and a pre-aborted signal hung the request until transport failure because llrt's fetch only subscribes to future abort sends andAbortSignal.any's already-aborted early-return never fires the sender. Both look like candidate upstream llrt reports.Cost: release binary grows 16.9 MB → 21.0 MB (hyper, rustls + ring, webpki root store, llrt modules). Known minor residue: each early-finished fetch parks its 30 s deadline timer until it lapses (bounded by fetch rate, cleaned on context teardown); a timers upgrade with real
clearTimeoutwould remove it and is left out of scope here.Verification:
cargo fmt --check,just lint,just lint-pedantic(0 warnings), 338 Rust tests across 16 binaries (newtests/runtime_globals.rspins the per-capability global surface, node:fs read/write end-to-end, cap gating, the patchedAbortSignal.timeout, and both fetch-guard regressions), 260 plugin vitest tests, full-plugin smoke test in the real runtime, release build. A five-lens review workflow (security, soundness, migration correctness, docs accuracy, CI/build) ran over the full diff; all five confirmed findings are fixed here or documented above.Follow-up: broader llrt surface + highbeam:platform removal
After the initial migration, more llrt modules were wired up and one custom module retired. Each addition was verified against llrt source first (init signature, deps, soundness, binary cost) and gets shape-test pins plus functional probes.
New always-on globals (uncapped, pure compute):
crypto: WebCrypto (getRandomValues,randomUUID,subtle.*) plus node-stylerandomBytes/createHash/createHmac(crypto-rust backend, no C).Temporal: full surface.Intl: partial,DateTimeFormat+supportedValuesOfonly (that is all llrt_intl ships, noNumberFormat).New uncapped
node:*modules:node:os,node:zlib(forced tocompression-rust, C-free),node:string_decoder.New
subprocesscapability: gatesnode:child_process(spawn) and theprocessglobal.process.envis a live proxy over the daemon's real environment;process.exitis inert here (sets__exitCode, which the host does not act on), so a plugin cannot terminate the daemon through it.child_processstdio rides llrt_stream's Rust stream constructors directly, nonode:streamregistration needed.Removed
highbeam:platform: superseded bynode:os. The 6 plugins that used it (app-launcher, kill-process, file-search, dictionary-linux, prefpanes, window-mgmt) now derive platform checks fromos.platform()("darwin"/"linux", not the old"macos"); their vitest suites mocknode:os.Intl and Temporal share
jiff(no heavy ICU). Verification after the follow-up: fmt +just lint+ pedantic (0 warnings), 354 Rust tests across 17 binaries, 260 plugin vitest tests, full-plugin smoke test in the real runtime.🤖 Generated with Claude Code