SkillAgentSearch skills...

Quicksand

Sandboxed JavaScript execution for Elixir via QuickJS-NG

Install / Use

/learn @lpgauth/Quicksand
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Quicksand

Sandboxed JavaScript execution for Elixir via QuickJS-NG.

Quicksand embeds the QuickJS-NG engine as a Rustler NIF, giving you in-process JS evaluation with strict memory and time limits. Each runtime runs on a dedicated OS thread — JS execution never blocks BEAM schedulers.

Features

  • Sandboxed JS with no filesystem, network, or OS access
  • Configurable memory limit, execution timeout, and stack size
  • Pre-registered Elixir callbacks callable from JS
  • Direct Erlang term <-> JS value conversion (no JSON serialization)
  • Resource-only API (no GenServer overhead)

Requirements

  • Elixir >= 1.15
  • Precompiled NIF binaries are provided for macOS (ARM/Intel) and Linux (x86/ARM)
  • Rust toolchain only needed if building from source

Installation

Add to your mix.exs:

def deps do
  [
    {:quicksand, "~> 0.1.0"}
  ]
end

Precompiled binaries will be downloaded automatically. To build from source instead:

QUICKSAND_BUILD=true mix deps.compile quicksand

Usage

Basic Evaluation

{:ok, rt} = Quicksand.start()

{:ok, 3} = Quicksand.eval(rt, "1 + 2")
{:ok, "hello"} = Quicksand.eval(rt, "'hello'")
{:ok, %{"a" => 1}} = Quicksand.eval(rt, "({a: 1})")

:ok = Quicksand.stop(rt)

Resource Limits

{:ok, rt} = Quicksand.start(
  timeout: 5_000,            # 5 seconds max execution time
  memory_limit: 10_000_000,  # ~10 MB heap limit
  max_stack_size: 512_000    # 512 KB stack
)

# Infinite loops are interrupted
{:error, "timeout"} = Quicksand.eval(rt, "while(true) {}")

# Runtime remains usable after timeout
{:ok, 42} = Quicksand.eval(rt, "42")

Callbacks

Register Elixir functions that JS code can call synchronously:

{:ok, rt} = Quicksand.start()

callbacks = %{
  "fetch_user" => fn [id] ->
    user = MyApp.Repo.get!(User, id)
    {:ok, %{"name" => user.name, "email" => user.email}}
  end,
  "log" => fn [message] ->
    Logger.info("JS: #{message}")
    {:ok, nil}
  end
}

{:ok, "Alice"} = Quicksand.eval(rt, """
  const user = fetch_user(1);
  log("Found user: " + user.name);
  user.name;
""", callbacks)

Callbacks must return {:ok, value} or {:error, reason}:

  • {:ok, value} — value is converted to JS and returned to the caller
  • {:error, reason} — throws a JS exception with the reason as the message
callbacks = %{
  "risky" => fn [n] ->
    if n > 0, do: {:ok, n * 2}, else: {:error, "must be positive"}
  end
}

# JS can catch callback errors
{:ok, "must be positive"} = Quicksand.eval(rt, """
  try { risky(-1); } catch(e) { e.message; }
""", callbacks)

Lifecycle

{:ok, rt} = Quicksand.start()

Quicksand.alive?(rt)  # true

# Global state persists across evals
{:ok, 42} = Quicksand.eval(rt, "globalThis.x = 42")
{:ok, 42} = Quicksand.eval(rt, "x")

# Stop is idempotent
:ok = Quicksand.stop(rt)
:ok = Quicksand.stop(rt)

Quicksand.alive?(rt)  # false

# Eval on stopped runtime returns error (doesn't raise)
{:error, "dead_runtime"} = Quicksand.eval(rt, "1")

API

| Function | Description | |----------|-------------| | Quicksand.start(opts) | Start a new JS runtime on a dedicated OS thread | | Quicksand.eval(runtime, code) | Evaluate JS code, return the result | | Quicksand.eval(runtime, code, callbacks) | Evaluate with pre-registered Elixir callbacks | | Quicksand.alive?(runtime) | Check if a runtime is alive | | Quicksand.stop(runtime) | Stop a runtime (idempotent) |

Start Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | :timeout | integer (ms) | 30_000 | Max JS execution time per eval | | :memory_limit | integer (bytes) | 268_435_456 (256 MB) | Max JS heap allocation | | :max_stack_size | integer (bytes) | 1_048_576 (1 MB) | Max JS call stack size |

Type Conversion

JS to Elixir

| JavaScript | Elixir | |------------|--------| | null, undefined | nil | | true, false | true, false | | integer | integer | | float | float (integer if no fractional part) | | string | binary string | | Array | list | | Object | map (string keys) | | function | nil | | NaN, Infinity | nil |

Elixir to JS (callback results)

| Elixir | JavaScript | |--------|------------| | nil | null | | true, false | true, false | | integer | number | | float | number | | binary string | string | | atom | string | | list | Array | | map | Object |

License

MIT

View on GitHub
GitHub Stars4
CategoryDevelopment
Updated4d ago
Forks0

Languages

Rust

Security Score

90/100

Audited on Apr 1, 2026

No findings