@@ -55,12 +55,95 @@ Javy.IO.writeSync(1, new TextEncoder().encode(JSON.stringify({hello: "world"})))
5555| Compile a JS snippet | ` Javex.compile/2 ` |
5656| Run a compiled module with JSON I/O | ` Javex.run(mod, input) ` |
5757| Run with raw bytes | ` Javex.run(mod, bytes, encoding: :raw) ` |
58+ | Grant a host capability | ` Javex.run(mod, input, callbacks: %{"name" => &MyApp.callback/2}) ` |
5859| Cap fuel, memory, or timeout | ` Javex.run(mod, input, fuel: …, max_memory: …, timeout: …) ` |
5960| Set runtime defaults | ` Javex.Runtime.start_link(name: :strict, default_fuel: …, default_max_memory: …) ` |
6061| Inspect active calls and cache usage | ` Javex.Runtime.stats() ` |
6162
6263Persisting compiled modules? ` %Javex.Module{} ` is a plain struct — ` :erlang.term_to_binary/1 ` round-trips it, no helper needed.
6364
65+ ## Asynchronous host callbacks
66+
67+ ` Javex.Host.call(name, argument) ` returns a real Promise. Top-level ` await ` ,
68+ sequential calls and ` Promise.all ` work on both backends. Each run grants its own
69+ named callbacks; runtimes never carry grants or credentials across runs.
70+
71+ For example, expose a narrow HTTP capability, not an unrestricted proxy:
72+
73+ ``` elixir
74+ js = ~S"""
75+ const products = await Promise.all([
76+ Javex.Host.call("catalog.product", {id: "p123"}),
77+ Javex.Host.call("catalog.product", {id: "p456"})
78+ ]);
79+ Javy.IO.writeSync(1, new TextEncoder().encode(JSON.stringify(products)));
80+ """
81+
82+ {:ok , mod} = Javex .compile (js, runtime: :untrusted_js )
83+ Javex .run (mod, nil ,
84+ runtime: :untrusted_js ,
85+ callbacks: %{" catalog.product" => & MyApp .Catalog .product / 2 },
86+ callback_timeout: 1_500 ,
87+ max_callback_calls: 8 ,
88+ max_callback_concurrency: 2
89+ )
90+ ```
91+
92+ The application supplies ` MyApp.Catalog ` and its HTTP client. For example, with
93+ Finch installed and a ` MyApp.Finch ` pool supervised by the application:
94+
95+ ``` elixir
96+ defmodule MyApp .Catalog do
97+ def product (%{" id" => id}, %{deadline: deadline}) when is_binary (id) do
98+ if ! Regex .match? (~r/ \A [a-zA-Z0-9_-]{1,64}\z / , id), do: raise (" invalid product ID" )
99+ remaining = deadline - System .monotonic_time (:millisecond )
100+ if remaining <= 0 , do: raise (" deadline elapsed" )
101+
102+ # Neither the base URL nor credentials are chosen by JavaScript.
103+ token = Application .fetch_env! (:my_app , :catalog_token )
104+ request = Finch .build (:get , " https://catalog.example.com/products/" <> id,
105+ [{" authorization" , " Bearer " <> token}])
106+
107+ {:ok , response} = Finch .stream (request, MyApp .Finch , %{status: nil , body: " " }, fn
108+ {:status , status}, acc -> %{acc | status: status}
109+ {:data , chunk}, acc ->
110+ if byte_size (acc.body) + byte_size (chunk) > 65_536 or
111+ System .monotonic_time (:millisecond ) >= deadline,
112+ do: raise (" response budget exceeded" )
113+ %{acc | body: acc.body <> chunk}
114+ _ , acc -> acc
115+ end , pool_timeout: remaining, receive_timeout: remaining)
116+
117+ {:ok , %{" status" => response.status, " body" => response.body}}
118+ end
119+ end
120+ ```
121+
122+ Finch does not follow redirects automatically. Keep destination, methods,
123+ redirect/DNS policy and credentials in trusted host code. The example bounds
124+ the body while streaming; checking its size after buffering would be too late.
125+ HTTP is not a Javex dependency, and ` fetch ` is not installed: the generic bridge
126+ does not implement the browser Request/Response, streaming or AbortSignal APIs.
127+
128+ Callbacks receive one JSON value and an absolute monotonic millisecond deadline.
129+ Return ` {:ok, json} ` or ` {:error, %{"code" => code, "message" => safe_message}} ` .
130+ JSON objects use string keys; encode binary bodies explicitly (for example base64),
131+ and use strings for integers beyond JavaScript's safe range. Names are non-empty
132+ UTF-8 strings of at most 128 bytes, with at most 128 grants per run.
133+
134+ Declared errors reject with ` HostError ` (` code ` and ` message ` ). Missing grants use
135+ ` capability_denied ` ; crashes and invalid results use sanitized ` callback_failed `
136+ messages. Rejections propagated to the root module fail with ` :js_error ` .
137+ Callback limits and timeouts terminate the run even if JavaScript catches errors.
138+ Concurrency limits reject excess work rather than building an unbounded queue.
139+
140+ Execution ends when the root module evaluation settles. Await all required work;
141+ detached callbacks are cancelled before a successful run returns. Caller death,
142+ runtime shutdown and deadlines also stop owned callback processes. Host callbacks
143+ run outside the guest's memory and OS limits. Propagate their deadline to your
144+ client and cancel separately owned tasks; killing a callback cannot undo an HTTP
145+ request already received by a server. There is no per-call AbortSignal API yet.
146+
64147## 🛡 Safety knobs
65148
66149``` elixir
@@ -93,6 +176,10 @@ Jason errors; sandbox failures return `Javex.RuntimeError`:
93176| ` max_input ` , ` max_output ` | 1 MiB each |
94177| ` max_stderr ` | 64 KiB diagnostics |
95178| ` max_module ` | 8 MiB input Wasm |
179+ | ` max_callback_calls ` | 32 attempts per run; maximum 1,024; zero disables callbacks |
180+ | ` max_callback_concurrency ` | 4 outstanding calls; maximum 1,024 |
181+ | ` callback_timeout ` | 1,000 ms per callback, bounded by the run deadline |
182+ | ` max_callback_input ` , ` max_callback_output ` | 64 KiB argument / 256 KiB result or error; maximum 8 MiB each |
96183| Runtime ` max_concurrency ` | 4 active calls; excess calls return ` :busy ` |
97184| Runtime ` max_cached_modules ` , ` max_cache_bytes ` | 64 modules / 64 MiB compiled images, LRU eviction |
98185
@@ -157,11 +244,14 @@ and cold compilation costs apply on every call. IPC requests/responses have a
157244 are configured. The host environment is never inherited, no filesystem
158245 directories are preopened, and no network capability is granted.
159246- The bundled JavaScript API provides synchronous ` Javy.IO ` , TextEncoder and
160- TextDecoder, and standard QuickJS intrinsics. It is ** not Node or Deno** :
161- there is no ` fetch ` , filesystem/path API, external module loading, timers,
162- or Promise event loop. Pending Promise jobs fail rather than running later.
247+ TextDecoder, standard QuickJS intrinsics, Promises, top-level ` await ` , and
248+ explicit ` Javex.Host.call ` capabilities. It is ** not Node or Deno** :
249+ there is no ` fetch ` , filesystem/path API, external module loading or timers.
250+ A root awaiting a Promise with no possible completion reaches the run deadline.
163251 WASI environment pairs are not a JavaScript ` process.env ` API.
164- - Clocks and randomness are not deterministic. The module provider hash checks
252+ - Runtime clocks are not deterministic. Snapshot-seeded ` Math.random ` can repeat
253+ across instances and is not cryptographically secure; obtain security-sensitive
254+ randomness from a host callback. The module provider hash checks
165255 compatibility, not authenticity. Persist only trusted module metadata and
166256 use ` :erlang.binary_to_term(bytes, [:safe]) ` only for trusted serialized data.
167257- The default ` :in_process ` backend is for trusted workloads: native JIT work
@@ -172,7 +262,7 @@ and cold compilation costs apply on every call. IPC requests/responses have a
172262 containers to cap the whole application. Process isolation assumes a trusted
173263 Linux kernel and does not eliminate kernel vulnerabilities or side channels.
174264
175- Javex supplies bounded synchronous computation. Workflow persistence, retries,
265+ Javex supplies bounded computation and explicitly granted host calls . Workflow persistence, retries,
176266credentials, tools, permissions and external effects belong in the automation
177267layer above it. Do not execute agent-generated JS as trusted Elixir host code.
178268
@@ -181,27 +271,26 @@ layer above it. Do not execute agent-generated JS as trusted Elixir host code.
181271- Compilation runs the provider's ` compile-src ` ABI inside a fuel/memory/deadline-limited Wasm store, then emits a small wrapper (or embeds the provider in static mode). No JS runs as native host code.
182272- In-process runtimes share a Wasmtime engine and one epoch clock thread. Each owns a compiled provider, bounded worker admission, and a bounded native-module cache. Isolated runtimes instead launch fresh OS-restricted workers.
183273- Every ` run/3 ` creates a fresh Wasmtime ` Store ` and provider instance. The cache avoids repeat native compilation, including for modules whose execution fails.
274+ - The resumable provider returns control between Promise continuations. Elixir runs callbacks in monitored processes, then resumes the same store with correlated results. I/O waits occupy no dirty NIF scheduler. Isolated workers exchange bounded callback/completion frames; their OS network restrictions remain unchanged.
184275- The Rust NIF and worker executable ship ** precompiled** for macOS (Apple Silicon + Intel) and Linux (aarch64 + x86_64 GNU). The isolated backend is Linux-only. No Rust toolchain required to install.
185276
186277### Bundled provider provenance
187278
188- ` priv/javy_plugin.wasm ` is the initialized default provider from
189- [ Javy v8.1.1] ( https://github.com/bytecodealliance/javy/releases/tag/v8.1.1 ) , using
190- namespace ` javy-default-plugin-v3 ` . The bundled bytes match the decompressed
191- official ` plugin.wasm.gz ` asset exactly. Reproduce or verify them with:
279+ ` priv/javy_plugin.wasm ` is Javex's custom resumable provider, built from the pinned
280+ Javy 7.0.0 runtime used by [ Javy CLI v8.1.1] ( https://github.com/bytecodealliance/javy/releases/tag/v8.1.1 ) .
281+ It is ** not** the upstream default provider. Source, locked dependencies,
282+ build instructions and the versioned ABI are in ` native/javex_plugin/ ` .
283+ Install the mise toolchain, clang/libclang and Javy CLI 8.1.1, then rebuild with:
192284
193285``` sh
194- curl -fL https://github.com/bytecodealliance/javy/releases/download/v8.1.1/plugin.wasm.gz -o /tmp/javy-plugin.wasm.gz
195- echo ' 52e3c83235f7735cb294e119a1a4b7be0a503323d5b8967eff8c0e516f3da8e1 /tmp/javy-plugin.wasm.gz' | sha256sum -c -
196- gzip -dc /tmp/javy-plugin.wasm.gz > /tmp/javy-plugin.wasm
197- echo ' 9a1f5310999888e39b7712e43384291cdc8cf84eaa409faf5338959d901ecacc /tmp/javy-plugin.wasm' | sha256sum -c -
198- cmp /tmp/javy-plugin.wasm priv/javy_plugin.wasm
286+ bash native/javex_plugin/build.sh
199287```
200288
201289Provider changes require compatibility tests and recompilation of dynamic
202290modules. Custom providers must be trusted, initialized Javy providers supporting
203291the same ` cabi_realloc ` / ` compile-src ` / ` invoke ` ABI; the namespace alone does
204- not establish compatibility or trust.
292+ not establish compatibility or trust. Providers without Javex's resumable ABI
293+ retain synchronous execution and do not provide ` Javex.Host ` .
205294
206295## Development and verification
207296
0 commit comments