In-process executor on ex_zapcode, a pure-Rust TypeScript-subset interpreter shipped as a NIF. No subprocess, no runtime binary in the image, no container config — a hex dependency is the whole deployment story, and the engine enforces a hard cap on live guest memory (the blocker that ruled Deno out for some hosts).
The engine has no host bindings at all (no filesystem/network/env), so
the :request callback is the sandbox's only door out, same as Deno.
Guest code suspends at each apis.<name>.request(...) call
(zapcode's start/resume model), the callback runs in Elixir, and the run
resumes with its return value. The apis object itself is built by a JS
preamble prepended to the code — one entry per globals["apiNames"] name,
each forwarding to the single registered external function.
Requires zapcode >= engine commit eaa546a: earlier revisions clobber the
apis base variable on the second namespaced call and cannot snapshot
with a pending builtin call (Promise.all) on the operand stack — both
found integrating this module, both covered by regression tests here.
Known divergences from OapiCodemode.Executor.Deno, all inherent to the
engine rather than this module:
- Callbacks are serial.
Promise.allover api calls works but resolves one call at a time (accepted zapcode constraint): wall-clock for N calls is the sum, not the max. - Callback errors abort the run.
resumecan only inject a return value, not a throwable, so a raising callback comes back to the model as this run's{:error, %{message: ..., logs: ...}}instead of a JS exception the guest could catch. - Logs are partial. zapcode-core does not capture stdout produced
after a resume, and its error tuple carries no stdout — so
console.logoutput after the first API call, or in a failing segment, is dropped. Output before the first suspension is captured. - No regex. Regex literals are rejected at parse time; guest code
must filter with
.includes()/.startsWith()/.endsWith().
Timeout is a wall-clock deadline enforced at every suspension point, and
the engine's own max_duration_secs (set to the same :timeout) bounds
guest execution inside a segment — so a run that never suspends is cut by
the engine at timeout, and one that suspends is cut at the first
suspension past the deadline. Worst case is just under 2× timeout (a
segment entered right before the deadline gets a fresh engine allowance).
A callback still in flight at the deadline is killed before returning —
it runs in an unlinked, monitored worker precisely so it can be cancelled
and so nothing (EXITs, DOWNs, straggler replies) leaks into a trap_exit
caller's mailbox after run/3 returns.
Callback results are round-tripped through JSON before entering the sandbox, keeping the behaviour's JSON-native boundary guarantee exact.