Raxol. REPL. Evaluator
(Raxol v2.8.0)
View Source
Elixir code evaluator with timeout protection, IO capture, and persistent bindings.
Each evaluator maintains its own binding context across evaluations, so variables defined in one eval call are available in the next.
evaluator = Evaluator.new()
{:ok, result, evaluator} = Evaluator.eval(evaluator, "x = 1 + 2")
{:ok, result, evaluator} = Evaluator.eval(evaluator, "x * 10")
result.value #=> 30Trust boundary
There is none here. eval/3 hands the code to Code.eval_string/3 with
base_env/0, which is __ENV__ with :file and :line overridden: an
unrestricted Macro.Env. No AST is inspected and no module is denied.
Raxol.REPL.Sandbox is a separate check a caller makes BEFORE calling
eval/3 -- the anonymously served playground demo does
(Raxol.Playground.Demos.ReplDemo gates on Sandbox.check(code, :strict))
-- and nothing this module does contributes to it.
The caps bound one process's resource use:
:timeout-- how long the single evaluation process may run:max_heap_bytes-- that process's heap, via:max_heap_sizewithkill: trueandinclude_shared_binaries: true, so off-heap (refc) binaries count too (without itString.duplicate("x", 200_000_000)returned under ReplDemo's 8 MB cap). The VM checks this cap at garbage collection, so it bounds what the process HOLDS, not one allocation: a builtin that allocates its whole result first (IO.iodata_to_binary/1over a large iolist,Tuple.duplicate/2) builds it before the kill, and one the allocator cannot satisfy aborts the node.Raxol.REPL.Sandboxrefuses the ones measured so far at:strict; only OS-level limits close the class (issue #1231):max_result_bytes-- the value plus bindings plus output it may hand back (refused when over), and the error message (truncated when over)Raxol.REPL.CaptureIO's limit -- bytes of captured output retained, and the wait on anio_libexpansion run on the evaluation's behalf
They bound nothing the evaluation creates or reaches. Filesystem reads and
writes, :os.cmd/1, Port.open/2, Node.connect/1 and :erlang.halt/0
resolve exactly as any other call does. The timeout and the heap cap are
both enforced by the VM: stop_eval/3 signals :kill, the one reason an
evaluation that sets Process.flag(:trap_exit, true) cannot intercept, so
no evaluation outlives its own timeout. It signals ONE pid, though -- a
process the evaluation spawned is not linked to it and keeps running with
full node authority after eval/3 has returned
{:error, "Evaluation timed out after ...", evaluator}.
So code handed to this evaluator runs with the full authority of the node's OS user. Expose the surface only to a principal already trusted with that uid. An AST allowlist standing in front of it is the whole of the boundary on an anonymous surface, and a gap in the allowlist is an escape; real confinement wants separate OS uids or containers, and this is one BEAM, one uid. The open design question -- stop evaluating untrusted code on the exposed surfaces, or add per-tenant OS isolation -- is issue #1033.
Summary
Functions
Returns the list of current variable bindings as [{name, value}].
Clears evaluation history, keeping bindings.
Evaluates code in the evaluator's binding context.
Returns evaluation history as [{code, result}], newest first.
Creates a new evaluator with an empty binding context.
Resets all bindings, keeping history.
Enable VFS helpers for this evaluator.
Types
Functions
Returns the list of current variable bindings as [{name, value}].
Clears evaluation history, keeping bindings.
Evaluates code in the evaluator's binding context.
Returns {:ok, result, new_evaluator} on success or {:error, reason, evaluator} on failure.
Bindings persist across calls. IO output is captured separately from the return value.
Returns evaluation history as [{code, result}], newest first.
Creates a new evaluator with an empty binding context.
Resets all bindings, keeping history.
Enable VFS helpers for this evaluator.
Seeds a vfs binding with a fresh virtual filesystem and auto-imports
Raxol.REPL.VfsHelpers so shell-like commands are available directly:
evaluator = Evaluator.new() |> Evaluator.with_vfs()
{:ok, _, evaluator} = Evaluator.eval(evaluator, "vfs = mkdir(vfs, \"/docs\")")
{:ok, _, evaluator} = Evaluator.eval(evaluator, "vfs = ls(vfs)")