afterburner / docs / introduction

Afterburner Docs.

One sandboxed, deterministic runtime for every language. Write C, C++, Rust, Go, TypeScript, JavaScript, Python, or Ruby; burn compiles each to WebAssembly and runs it under the same hard resource limits and the same capability gates. Every limit, every door, configurable. Sealed by default.

v0.1 · polyglot every language WASM everywhere MSRV 1.90 · 2024 edition Apache-2.0

Every language.
One WASM sandbox.

Afterburner ships a burn CLI that runs any supported language, plus an embeddable Rust crate behind one ergonomic builder. Everything compiles to WebAssembly and executes in a single deterministic, capability-sealed runtime. The defaults are safe; the limits are yours.

$ curl -fsSL https://afterburner.sh | sh · Click to copy

#Introduction

Afterburner is a polyglot runtime. You give it a source file or a package in any supported language - C, C++, Rust, Go, TypeScript, JavaScript, Python, or Ruby - and it compiles that code to WebAssembly and runs it inside a single sandbox. No matter the language, the same three guarantees hold: execution is deterministic (a NaN is a NaN on every host, threads are off, work is fuel-metered), the sandbox is capability-sealed by default (no network, no filesystem, no environment until you grant it), and resource limits are hard (memory, fuel, wall-clock).

There are two ways to build on it. The fast path is the burn CLI: burn run app.go runs a Go file, burn run main.rs a Rust file, burn repl --lang python opens an interactive session, and burn compile turns any program into a single portable wasm artifact that burn run executes anywhere with no recompile and zero config. The other path is the embeddable afterburner crate, which exposes the same engine to your own Rust program behind one builder (see the Programmatic API).

New here? Install, then run a file in any language. To author a publishable unit, read Your first package and the per-language guide under Supported languages.

i
All eight languages ship today.

JavaScript, TypeScript, Rust, Go, C, C++, Python, and Ruby run end to end: burn run, burn repl, and burn compile all work with zero configuration. Python uses an auto-fetched (or embedded) Python runtime; numpy and pandas import and execute. Ruby compiles to a self-contained wasm32-wasip1 module, auto-fetched on first use. The one honest boundary: a dependency with no wasm build is rejected at compile time with a clear message.

#Installation

The fastest path: afterburner.sh dispatches by user agent, verifies the SHA-256 sidecar, and drops burn into ~/.local/bin. Pick your shell:

$ Copy

Pin a version with BURN_VERSION=vX.Y.Z · custom install dir with BURN_INSTALL=/usr/local/bin · skip the $PATH update with BURN_INSTALL_NO_PATH=1

Uninstall - undoes exactly what the installer did (binary + the $PATH line), nothing else: curl -fsSL https://afterburner.sh/uninstall | sh · PowerShell: iwr -useb https://afterburner.sh/uninstall | iex

Verify the binary, then check which backends were built in:

shell
$ burn version
afterburner 0.1.3  (features: release-cli, wasm, native, adaptive, flow, thrust)

Or pull the runtime as a Rust library, or build the burn CLI from source:

shell
$ cargo add afterburner                          # embed the engine in a Rust project
$ cargo install afterburner --features bin       # build the burn CLI from crates.io
i
What you need at runtime, what you need to compile.

The prebuilt binary is self-contained: nothing external is required to run JavaScript, TypeScript, or Python. Compiling a native language reaches for that language's own toolchain on your $PATH: cargo (Rust), go (Go), and clang with a WASI sysroot (C / C++). The optional JS/TS precompile path uses a build-time tool that is never required at runtime. See each language's section for the exact tool.

#Run any language

burn run <file> reads the file extension, compiles to WebAssembly, and executes - no project, no config. The extension is the language declaration.

shell
$ burn run app.js          # JavaScript
$ burn run app.ts          # TypeScript (types stripped via oxc)
$ burn run main.rs         # Rust
$ burn run main.go         # Go
$ burn run main.c          # C
$ burn run main.cpp        # C++

# Run the current package's entry (from afb.toml) - no file argument
$ burn run

# Args after the file land in the program's argv
$ burn run main.go --name world
i
Python and Ruby single-file run.

burn run main.py and burn run main.rb work. The Python and Ruby runtimes are auto-fetched into ~/.burn on first use, or are already embedded in the release binary. No environment variables or manual setup required. See Python and Ruby.

For JavaScript you can also evaluate inline, pipe data through a UDF, parse without running, or benchmark:

shell
$ burn -e 'module.exports = () => 42'           # eval inline JS
$ echo '{"n":21}' | burn thrust double.js       # UDF: stdin JSON in, stdout JSON out
$ burn check app.ts                          # compile + parse only; exit 0 / 1
$ burn bench perf.js --iters 10000 --workers 8

#The REPL

An interactive read-eval-print loop for any supported language. The default is JavaScript; --lang selects another. JS and TypeScript run on the engine; Rust, Go, C, and C++ compile and run each line (honest about the per-line compile cost and the toolchain it needs); Python runs over the bundled Python runtime.

shell
$ burn repl                  # JavaScript (the default)
$ burn repl --lang ts        # TypeScript: each line transpiled
$ burn repl --lang rust      # compile-and-run per line (needs cargo / rustc)
$ burn repl --lang go        # compile-and-run per line (needs go)
$ burn repl --lang c         # compile-and-run per line (needs clang + WASI sysroot)
$ burn repl --lang cpp       # compile-and-run per line
$ burn repl --lang python    # line REPL over the Python runtime
$ burn repl --lang ruby      # line REPL over the Ruby runtime

Accepted --lang values (aliases included): js, javascript, ts, typescript, rust, go, golang, c, cpp, c++, cxx, cc, python, py, ruby, rb. The default is js.

i
How the Python REPL stays stateful.

The Python runtime runs a fresh interpreter per call, so the session accumulates the lines you have entered and re-runs the whole program each line, showing only the new output suffix. Assignments, def, and import persist for free; pure compute is exact. The runtime auto-fetches into ~/.burn on first use; BURN_PYTHON_RUNTIME is an optional developer override to point at a local directory instead. burn repl --lang ruby opens an interactive Ruby session in the same way.

#Your first package

A package is a capability-sealed unit you scaffold, test, build into a single .afb, and publish. Scaffold one - pick the language with --lang:

shell
$ burn init ./greeter --namespace nyquist --name greeter
✓ created nyquist/greeter
  afb.toml  manifold.json  source/main.js  tests/  README.md

$ burn init ./hot --namespace nyquist --name hot --lang rust   # a Rust package
$ burn new nyquist/widget                                     # scaffold into ./widget

The language field in afb.toml drives compilation; --lang accepts the same values as the REPL (default js; --ts is shorthand for --lang typescript). Open the entry under source/, write your code, then test, build, and publish:

shell
$ burn test          # run every file under tests/ in the sandbox
$ burn compile       # build nyquist-greeter-0.1.0.afb (content-addressed)
$ burn publish       # upload to the registry

That is the whole loop. It is sealed by default: the scaffolded manifold.json grants nothing, so the code cannot touch network, filesystem, or environment until you open a door. The Packages section is the full authoring reference; Supported languages covers each language's source layout and how multi-module works.

#Compile to a portable wasm artifact

burn compile turns a program in any supported language into a single portable artifact that burn run executes with no recompile and zero config. Drop it in a container, put it in a release pipeline, or cache it by digest. The runtimes auto-fetch on first use (or are embedded in the release binary); nothing needs to be installed on the target host beyond burn itself.

Per-language output

The artifact shape differs per language family, and the docs are honest about that:

Language(s)ArtifactNotes
JavaScript / TypeScriptA single .wasm module (JS engine compiled in)The JS/TS engine is baked into the module; no external runtime at execution time.
RubyA single wasm32-wasip1 module (Ruby interpreter + source + stdlib + gems, packed into one portable artifact)The Ruby runtime payload is auto-fetched into ~/.burn on first compile or embedded in the release binary. No setup required.
PythonA self-contained .afb bundle (Python runtime + stdlib + source + wheels)Python is a bundle, not a bare .wasm, because the Python runtime loads its filesystem at startup. It is still one portable artifact burn run executes with no recompile.
Rust / Go / C / C++A native compile to wasm32-wasip1The compiled output is a self-contained WASM module; no language toolchain on the target host.

Dependency bundling

Pure-language deps and deps that have a wasm build are bundled in. A dependency with only a native (non-wasm) build is rejected at compile time with a clear message - this is a known boundary, not a silent failure:

shell
# error: numpy-1.23 has no wasm32 wheel; use a wasm-compatible version or pin one that does
$ burn compile main.py
error: dependency 'old-native-only-pkg' has no wasm32 build; rebuild with a wasm-compatible version

Zero config

A user runs burn compile and burn run with no environment variables and nothing extra to install. On the first compile for a language that needs an embedded runtime (Python, JS/TS), burn auto-fetches and caches the runtime payload. The prebuilt release binary already embeds the runtimes it can, so most installs never need a network fetch.

shell
# Compile a Rust program to a portable wasm artifact
$ burn compile main.rs -o main.afb
$ burn run main.afb              # no recompile, zero config

# Compile a TypeScript program (JS engine baked in)
$ burn compile app.ts -o app.afb
$ burn run app.afb

# Compile a Python program (self-contained .afb bundle - not a bare .wasm)
$ burn compile main.py -o main.afb
$ burn run main.afb

# Compile a package (any language)
$ burn compile                   # reads afb.toml, writes <name>-<version>.afb
$ burn run my-pkg-0.1.0.afb
i
Python is a bundle, not a bare wasm.

The Python runtime loads a filesystem image at startup, so the artifact is a self-contained .afb bundle rather than a single bare .wasm file. It is still one portable artifact: burn run main.afb works anywhere with no recompile and no extra config. The distinction is honest, not a flaw.

#Supported languages

Every language compiles to a WebAssembly module and runs in the same sandbox. The table is the at-a-glance status; each language has its own guide below. Jump to one:

Languagelanguage valueEntryRun a fileREPLCompile / package
JavaScriptjs · javascriptsource/main.jsYesYesYes (engine; precompile optional)
TypeScriptts · typescriptsource/main.tsYesYesYes (oxc strips types)
Rustrustsource/main.rsYesYesYes (cargo / rustc)
Gogo · golangsource/main.goYesYesYes (go build)
Ccsource/main.cYesYesYes (clang + WASI)
C++cpp · c++ · cxx · ccsource/main.cppYesYesYes (clang++ + WASI)
Pythonpython · pysource/main.pyYesYesYes (auto-fetched)
Rubyruby · rbsource/main.rbYesYesYes (auto-fetched)

The entry layout is the same shape in every language: a source/ directory with the entry file named by [package] entry, and everything in source/ packed verbatim. Multi-module follows each language's own convention. The tabbed sample below is the same fib(20) + sum(1..=100) program in each language (drawn from examples/languages):

source/main.js
function fib(n) {
  if (n < 2) return n;
  let a = 0, b = 1;
  for (let i = 2; i <= n; i++) { const c = a + b; a = b; b = c; }
  return b;
}
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
console.log(`js: sum(1..=100)=${sum} fib(20)=${fib(20)}`);
source/main.ts
function fib(n: number): number {
  if (n < 2) return n;
  let a = 0, b = 1;
  for (let i = 2; i <= n; i++) { const c = a + b; a = b; b = c; }
  return b;
}
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
console.log(`ts: sum(1..=100)=${sum} fib(20)=${fib(20)}`);
source/main.rs
fn fib(n: u64) -> u64 {
    if n < 2 { return n; }
    let (mut a, mut b) = (0u64, 1u64);
    for _ in 2..=n { let c = a + b; a = b; b = c; }
    b
}
fn main() {
    let sum: u64 = (1u64..=100).sum();
    println!("rust: sum(1..=100)={sum} fib(20)={}", fib(20));
}
source/main.go
package main

import "fmt"

func fib(n uint64) uint64 {
	if n < 2 { return n }
	a, b := uint64(0), uint64(1)
	for i := uint64(2); i <= n; i++ { a, b = b, a+b }
	return b
}

func main() {
	var sum uint64
	for i := uint64(1); i <= 100; i++ { sum += i }
	fmt.Printf("go: sum(1..=100)=%d fib(20)=%d\n", sum, fib(20))
}
source/main.c
#include <stdio.h>

unsigned long long fib(unsigned long long n) {
    if (n < 2) return n;
    unsigned long long a = 0, b = 1;
    for (unsigned long long i = 2; i <= n; i++) { unsigned long long c = a + b; a = b; b = c; }
    return b;
}

int main(void) {
    unsigned long long sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    printf("c: sum(1..=100)=%llu fib(20)=%llu\n", sum, fib(20));
    return 0;
}
source/main.cpp
#include <cstdio>
#include <cstdint>

uint64_t fib(uint64_t n) {
    if (n < 2) return n;
    uint64_t a = 0, b = 1;
    for (uint64_t i = 2; i <= n; i++) { uint64_t c = a + b; a = b; b = c; }
    return b;
}

int main() {
    uint64_t sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    std::printf("cpp: sum(1..=100)=%llu fib(20)=%llu\n",
                (unsigned long long)sum, (unsigned long long)fib(20));
    return 0;
}
source/main.py
def fib(n):
    if n < 2:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

total = sum(range(1, 101))
print(f"python: sum(1..=100)={total} fib(20)={fib(20)}")
source/main.rb
def fib(n)
  return n if n < 2
  a, b = 0, 1
  (2..n).each { a, b = b, a + b }
  b
end

total = (1..100).sum
puts "ruby: sum(1..=100)=#{total} fib(20)=#{fib(20)}"

JavaScript

language = "js" (alias javascript), entry source/main.js. JavaScript runs on the engine directly - no external toolchain to run it. burn compile can additionally precompile to WASM for sub-millisecond cold starts. A package's entry assigns its export with module.exports; sibling files are plain require('./lib/util'), and npm packages declared under [npm] are imported by bare specifier. See the Node compat surface for the built-in module list.

shell
$ burn run app.js
$ burn repl
$ burn init ./pkg --namespace ns --name app          # js is the default
$ burn compile                                       # .afb (+ precompiled WASM, opt-in)

TypeScript

language = "typescript" (alias ts), entry source/main.ts. Types are stripped to JavaScript with oxc at compile time (type-stripping, not a full tsc type-check), so the published module is plain WASM-bound JS. Everything from the JavaScript guide applies; scaffold with --ts (shorthand for --lang typescript), which also writes a tsconfig.json.

shell
$ burn run app.ts
$ burn repl --lang ts
$ burn init ./pkg --namespace ns --name app --ts

Rust

language = "rust", entry source/main.rs. Compiles to WebAssembly with cargo build (a single bare file goes through rustc) to a WASI command module. Multi-module is Cargo-resolved: a standalone Cargo.toml at the package root points [[bin]] path at source/main.rs, and the usual mod system resolves the tree under source/ - sibling files (source/geometry.rs via mod geometry;) and directory modules (source/stats/mod.rs via mod stats;).

Cargo.toml (package root, beside afb.toml)
# Its own [workspace] root detaches it from any enclosing cargo workspace.
[workspace]

[package]
name = "rust-multimodule"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "rust-multimodule"
path = "source/main.rs"
source/main.rs
mod geometry;   // source/geometry.rs
mod stats;      // source/stats/mod.rs (directory module)

fn main() {
    let area = geometry::rectangle_area(5, 10);   // pub fn; its private helper is unreachable
    let mean = stats::mean(&[10, 20, 30]);
    println!("area={area} mean={mean}");
}
shell
$ burn run main.rs            # single file via rustc
$ burn repl --lang rust       # compile-and-run per line
$ burn compile                # package -> WebAssembly module in the .afb

Go

language = "go" (alias golang), entry source/main.go. Compiles to WebAssembly with go build to a WASI command module. Multi-module uses Go modules: a go.mod at the package root names the module, and a second package lives in a subdirectory (source/geometry/geometry.go) imported by its module path. Go's capitalized-identifier rule is the export boundary.

go.mod (package root)
module go-multimodule

go 1.21
source/main.go
package main

import (
	"fmt"
	"go-multimodule/source/geometry"
)

func main() {
	area := geometry.RectangleArea(5, 10)        // exported (capitalized)
	perimeter := geometry.RectanglePerimeter(5, 10)
	fmt.Printf("area=%d perimeter=%d\n", area, perimeter)
}
shell
$ burn run main.go
$ burn repl --lang go
$ burn compile

C

language = "c", entry source/main.c. Compiles to WebAssembly with clang against the wasi-sdk sysroot (a Makefile or CMakeLists.txt in the package is honored when present). Multi-file C is just several translation units plus a shared header, all under source/: burn compiles every .c it finds and links them. static functions keep internal linkage as the encapsulation boundary.

source/ layout
source/
├─ main.c          # entry, #include "geometry.h"
├─ geometry.c      # second translation unit
└─ geometry.h      # shared header (the public interface)
source/main.c
#include <stdio.h>
#include "geometry.h"

int main(void) {
    int area = rectangle_area(5, 10);          // cross-unit call into the geometry API
    int values[] = {10, 20, 30};
    int avg = mean(values, 3);
    printf("area=%d mean=%d\n", area, avg);
    return 0;
}
shell
$ burn run main.c
$ burn repl --lang c
$ burn compile

C++

language = "cpp" (aliases c++, cxx, cc), entry source/main.cpp. Compiles to WebAssembly with clang++. Multi-file C++ mirrors C: translation units plus a header under source/, with a namespace and an anonymous-namespace boundary for the private helpers.

source/main.cpp
#include <cstdio>
#include "geometry.hpp"

int main() {
    const int area = geometry::rectangle_area(5, 10);
    const int values[] = {10, 20, 30};
    const int mean = geometry::mean(values, 3);
    std::printf("area=%d mean=%d\n", area, mean);
    return 0;
}
shell
$ burn run main.cpp
$ burn repl --lang cpp
$ burn compile

Python

language = "python" (alias py), entry source/main.py. Python runs on the Python runtime, which is auto-fetched into ~/.burn on first use (or already embedded in the release binary). numpy and pandas import and execute, so pandas.DataFrame({'a':[1,2,3]}).sum() works. All three paths work with no environment variables:

  • Run: burn run main.py - executes single-file Python, no setup.
  • REPL: burn repl --lang python - interactive session; assignments and imports persist across lines.
  • Compile: burn compile main.py - produces a self-contained .afb bundle (Python runtime + stdlib + source + wheels) that burn run main.afb executes anywhere.

The optional BURN_PYTHON_RUNTIME variable lets you point at a local runtime directory instead of the auto-fetched one. It is never required for normal use.

shell
$ burn run main.py
$ burn repl --lang python
>>> import numpy as np; np.arange(5).sum()
10
>>> import pandas as pd; pd.DataFrame({'a': [1, 2, 3]}).sum()
$ burn compile main.py -o main.afb
$ burn run main.afb

Ruby

language = "ruby" (alias rb), entry source/main.rb. Ruby compiles to a single wasm32-wasip1 module: the Ruby interpreter + source + stdlib + gems packed into one portable artifact. The Ruby runtime auto-fetches into ~/.burn on first use, or is embedded in the release binary. All three paths work with zero configuration:

  • Run: burn run main.rb - executes single-file Ruby, no setup.
  • REPL: burn repl --lang ruby - interactive Ruby session.
  • Compile: burn compile main.rb - produces a self-contained wasm32-wasip1 module that burn run executes anywhere.
shell
$ burn run main.rb
ruby: sum(1..=100)=5050 fib(20)=6765
$ burn repl --lang ruby
$ burn compile main.rb -o main.afb
$ burn run main.afb

#WASM everywhere

The unifying idea: every language is compiled to WebAssembly and run on one engine. C, C++, Rust, and Go become self-contained WASI command modules through their own toolchains; JavaScript and TypeScript run on an embedded engine (and can be precompiled to WASM ahead of time); Python runs on a bundled Python runtime; Ruby compiles to a single wasm32-wasip1 module. Because the execution substrate is the same for all of them, the sandbox and determinism guarantees below apply uniformly - there is no per-language carve-out.

WASI gives the program a small, explicit host interface: stdin, stdout, stderr, and the capabilities you grant. There is no ambient access to the host - a module cannot open a socket or read a file unless the capability sandbox hands it that door.

#Deterministic execution

The WASM engine is configured for reproducibility: the same module on the same input produces the same result on any host CPU. Three settings carry it.

PropertySettingWhat it guarantees
Canonical NaNNaN canonicalization onNaN bit patterns are canonicalized, so floating-point results are identical across CPUs that would otherwise emit different NaN payloads. (Relaxed-SIMD is pinned deterministic too.)
Threads offwasm_threads(false)Shared-memory multi-threading is disabled. A module that uses shared memories or wait / notify is refused at compile time, so there is no data-race nondeterminism inside a module.
Fuel meteringconsume_fuel(true)Every run is bounded by an instruction budget. A tight loop trips the gauge in deterministic op-time, not wall-clock time, so the cutoff is reproducible.

This engine lives in afterburner-wasi (deterministic_engine()). Fuel instrumentation is unconditional - it taxes even an unlimited-budget call, roughly a measured 19% on guest-CPU-bound work - because determinism is not an opt-in here.

#The capability sandbox

The runtime is sealed by default. A module - in any supported language - starts with zero host capabilities: no filesystem, no network, no environment, no subprocess, no inbound listening. You open exactly the doors the code needs, and nothing widens by accident. The capability set is a Manifold (the library's Manifold::sealed() is the default, and the test in the source asserts Manifold::default() == Manifold::sealed()).

CapabilityFieldSealed defaultVariants
FilesystemfsNoneNone · ReadOnly(roots) · ReadWrite(roots)
Network (out)netNoneNone · OutboundHttp(allow) · OutboundFull(allow)
EnvironmentenvNoneNone · AllowList(keys) · Full
Inbound listenlistenNoneNone · Ports([..]) · PortRange(lo, hi) · Any
Cryptocryptofalsebool
Subprocesschild_processfalsebool (native backend only; WASM rejects with PermissionDenied)
Process exitallow_exitfalsebool (whether process.exit(n) terminates the call)
HTTP timeouthttp_timeout_msNoneOption<u64> per-call cap on outbound HTTP

On the CLI you grant capabilities with --allow-*. By default the CLI runs open so existing Node scripts drop in without flags; pass --sandbox to flip to deny-by-default and then opt in. The library default is the opposite - sealed - so embedding never hands ambient access to untrusted code.

shell
$ burn run app.js                              # CLI default: open

$ burn --sandbox \                            # seal, then grant exactly these
       --allow-fs-read=./data \
       --allow-net=api.example.com,*.trusted.io \
       --allow-env=NODE_ENV,API_KEY \
       run app.js

$ burn -A run trusted.js                       # grant everything (trusted code only)

Allow-lists accept exact hostnames (api.example.com) and single-label wildcards (*.trusted.io); an optional :port pins an entry to one port (127.0.0.1:9000), and a port-less entry matches any port. Filesystem roots are resolved with canonicalize at call time, so a path escape via .. or a symlink outside the listed roots is rejected with PermissionDenied. OutboundHttp covers http / https / fetch; OutboundFull additionally permits raw TCP and TLS.

#Fuel, memory & timeouts

Three independent hard caps, on the CLI and on the builder:

shell · rust
# CLI
$ burn --fuel 50000000 --memory 268435456 --timeout 5000 run job.js

// embedded
let ab = Afterburner::builder()
    .fuel(your_op_budget)         // instruction-count budget (deterministic)
    .memory_bytes(your_memory_cap)  // per-call linear-memory ceiling
    .timeout_ms(your_deadline)      // wall-clock kill switch
    .build()?;
  • Fuel meters WASM instructions in deterministic op-time, not wall-clock, so the cutoff is reproducible across hosts.
  • Memory is enforced by the sandbox allocator; an over-budget allocation fails inside the module, never an OOM kill of the host.
  • Timeout is the wall-clock backstop for work that does not burn fuel (host I/O).
i
Nothing is hardcoded.

The runtime ships sensible defaults, but every limit is a knob you set per build, per call, or per CLI invocation. For long-running HTTP daemons the WASM pooling allocator pre-reserves a per-instance address range (1 GiB by default); raise or lower it with BURN_MAX_LINEAR_MEMORY (for example BURN_MAX_LINEAR_MEMORY=4G, clamped to the 4 GiB wasm32 ABI maximum). --memory sets a tighter per-call cap below that ceiling.

#Adaptive execution (Flying Start)

For JavaScript, the default mode boots fast and then settles into the safer path without you noticing: the first call runs on the native engine for sub-microsecond first-call latency, a background thread compiles the same script to WASM, and subsequent calls switch to the sandboxed WASM path.

Cold start
Sub-millisecond ignition path optimised for first-call latency.
Warm path
Fully sandboxed WASM execution with bytecode cached across calls.
Hot path
Pooled instances, fuel-precharged, microsecond steady-state per call.

To pin a single backend, pass --mode native|wasm|adaptive on the CLI or .mode(..) on the builder. adaptive is the default. The compiled languages (C/C++/Rust/Go) always execute as WASM; the native fast-start tier is a JavaScript-engine feature.

#Multi-threaded scheduling

One flag turns a single-threaded runtime into an N-worker scheduler. Bounded per-worker queues plus a global injector; token-bucket admission keeps the system responsive under load; steal-when-idle keeps cores busy without thrashing locality. The width is yours to choose.

rust
let ab = Afterburner::builder()
    .threaded(your_worker_count)    // any N >= 1
    .route_by(RouteBy::HashKey)    // pin sessions to a worker
    .build()?;

for row in rows {
    ab.submit(&id, &row, row.session_key())?;
}
ab.drain().await?;            // graceful shutdown

For HTTP daemons, burn serves requests from every CPU the OS gives the process; container CPU limits flow through automatically (docker run --cpus=4 gives 4-way parallelism). Override the worker count with BURN_SHARDS=N. Per-request JavaScript state is local to each worker, not global - use require('afterburner:state') for state shared across requests.

#Cross-invocation state

The custom afterburner:state module lets scripts share data across calls. The default InMemoryStateStore uses a lock-free map with atomic counters. Embedders can swap their own (Redis, SQLite, anything) via the StateStore trait.

javascript · script side
const state = require('afterburner:state');

state.setJSON('lastSeen', Date.now());
const hits = state.increment('hits');          // atomic i64
const seen = state.get('lastSeen');
const keys = state.listKeys('session:');          // prefix scan

module.exports = () => ({ hits, seen });

#Packages & dependencies

A .afb is a single content-addressed, compressed package: a manifest (afb.toml), a capability grant (manifold.json), and your source/ (plus an optional precompiled WASM module). Packages publish to a registry and resolve dependencies the way Cargo does - declared, version-resolved, digest-pinned, and cached - not vendored into your artifact.

Package anatomy

Every package is a directory. burn compile compresses it deterministically into a single .afb named for its SHA-256 content digest. The shape is the same across every language - only the entry extension and any language build file (a root Cargo.toml or go.mod) differ.

text
my-pkg/
├─ afb.toml          # manifest: identity, language, entry, dependencies
├─ manifold.json     # capability grant: what this code may touch
├─ source/
│  ├─ main.rs        # the entry (main.{js,ts,rs,go,c,cpp,py,rb})
│  └─ geometry.rs    # any number of sibling modules (language-native)
├─ Cargo.toml        # Rust only: build file at the package root
├─ tests/            # run by `burn test`
└─ README.md

Only source/, afb.toml, and manifold.json are packed. Everything under source/ is carried verbatim; dependencies are not packed - they are declared and resolved at install time.

afb.toml: the manifest

[format], [package], and [runtime] are required; [dependencies], [npm], and [metadata] are optional. The one field that changed with polyglot: language is now any of the eight values, not just JS or TypeScript.

toml
[format]
version = "1.0"                # .afb format version "MAJOR.MINOR" (current 1.x)

[package]
namespace = "nyquist"          # your registry namespace (required)
name      = "widget"           # package name; published as nyquist/widget
version   = "0.1.0"            # semver (required, validated)
language  = "rust"             # js·javascript·ts·typescript·rust·go·golang·c·cpp·c++·cxx·cc·python·py·ruby·rb
entry     = "source/main.rs"   # must live under source/ (required)
description = "One-line summary."   # optional, indexed by search
homepage   = "https://example.com" # optional
license    = "Apache-2.0"      # optional SPDX id
keywords   = ["geometry", "util"]  # optional, indexed by search

[runtime]
min = "0.1.0"                  # minimum afterburner-core version (required)
# target = "wasm32-wasip1"     # optional: precompiled module target (format minor >= 2)

[dependencies]                 # optional: other registry packages
"nyquist/json-tools" = "sha256:9f86d0…"

[npm]                          # optional: npm packages, semver ranges
date-fns = "^3"

[metadata]                     # optional: free-form, never interpreted by the reader
maintainer = "team@example.com"

Use burn add nyquist/json-tools to pin a registry dependency (it resolves the digest for you) and edit [npm] directly for npm packages - both install with burn install. The full key reference is in afb.toml keys.

manifold.json: the capability grant

The Manifold is the package's entire permission surface. It is sealed by default: a fresh package can touch nothing. Each field is deny-by-default, and an absent field stays sealed, so a manifold can never widen by omission. This is the on-disk form (the serialized Manifold):

manifold.json
{
  "fs": "None",                                  // or {"ReadOnly":["/data"]} / {"ReadWrite":["/tmp"]}
  "net": { "OutboundHttp": ["api.example.com"] },  // "None" | {"OutboundHttp":[hosts]} | {"OutboundFull":[hosts]}
  "env": "None",                                 // "None" | {"AllowList":["API_KEY"]} | "Full"
  "crypto": false,                              // allow crypto host module
  "child_process": false,                      // allow spawning subprocesses (native backend only)
  "allow_exit": false,                         // process.exit() ends the call vs throwing
  "http_timeout_ms": 30000,                   // optional per-request wall-clock cap
  "listen": "None"                              // "None" | {"Ports":[8080]} | {"PortRange":[8000,8100]} | "Any"
}

A freshly scaffolded package writes the minimal sealed form - {"fs":"None","net":"None","env":"None","crypto":false,"child_process":false}. The burn init --allow-net host --allow-fs-read /data flags write the open grants for you. The registry shows reviewers these grants before anyone installs.

Declaring dependencies

Two kinds, both declared in afb.toml and both resolved and cached by burn install - never copied into your package. A registry dependency value can take three shapes: a semver range, an exact sha256: pin, or a source table.

toml
[dependencies]
"nyquist/json-tools" = "^0.3"                                # semver range, resolved + digest-pinned
"nyquist/exact"      = "sha256:9f86d0…"                       # exact content-addressed pin
"nyquist/shared"     = { path = "../shared" }                  # local sibling, resolved at compile time
"nyquist/parser"     = { git = "https://github.com/x/parser", tag = "v1.2.0" }  # git: exactly one of tag/branch/rev

[npm]
leftpad  = "^1.0.0"                                                 # vendored into source/node_modules/** by burn install
date-fns = "^3"
shell
$ burn add nyquist/json-tools        # pin a registry package by digest
$ burn install                       # resolve the whole graph -> burn.lock
$ burn install --locked              # reuse the lockfile, skip resolution

burn install resolves the full transitive graph with a conflict-driven version solver (the class Cargo uses), topologically orders it, and writes burn.lock pinning every package to an exact content digest. Registry dependencies are pinned by SHA-256, so a coordinate can never silently resolve to different bytes; npm dependencies resolve to an exact version + integrity hash. Resolved packages land in a shared content-addressed cache, fetched at most once per machine. Importing is just require() (or a TS/JS import): a registry dependency by its namespace/name, an npm package by its bare name.

Python packages ([pip])

Python packages from PyPI are declared in the [pip] table in afb.toml. Each entry is package-name = "specifier" where the specifier is a PEP 440 version specifier string. burn install resolves the dependency closure, verifies each wheel's SHA-256 digest, and vendors pre-built sandbox-compatible wheels into vendor/pip/** inside the .afb - so the resolved wheels ship with the package, not fetched at runtime.

i
How packages are resolved.

Packages you declare in [pip] are resolved and vendored into the .afb ahead of time and mounted at startup; nothing is fetched mid-run. burn install resolves the dependency closure, verifies each wheel's SHA-256 digest, and embeds the pre-built sandbox-compatible wheels so they are available immediately when the program starts. A core scientific set (numpy, pandas, python-dateutil, pytz, six) is bundled and imports out of the box. This is what keeps a run sealed and reproducible: packages are declared and vendored, never fetched at runtime.

afb.toml
[pip]
requests = ">=2.31,<3"    # range: at least 2.31, less than 3
numpy    = "==1.26.4"      # exact pin
rich     = "~=13.6"        # compatible release: >= 13.6, == 13.*
httpx    = "*"             # any version
shell
$ burn install      # resolve + vendor [pip] wheels into vendor/pip/**
$ burn run          # import requests, numpy, etc. from the vendored wheels
i
Supported specifier forms (v1).

All standard PEP 440 operators are accepted: ==, !=, <, >, <=, >=, ~= (compatible release), === (arbitrary equality), and the bare * wildcard. The .* suffix is valid on == and != only (e.g. == 1.4.*). Comma-separated conjunctions are supported (e.g. >=2.31,<3).

Not supported in v1 (refused with an error): git requirements (git+https://...), direct URL requirements (https://...), local path requirements (./pkg, /abs/path), editable installs (-e), sdist/wheel file references (.tar.gz, .whl), PEP 508 environment markers (; python_version >= "3.8"), and extras (requests[security]). Use the base package name with a registry specifier instead.

The [dependencies] table is for other .afb packages (resolved at install time, not packed). The [pip] table is for PyPI packages (vendors wheels into the .afb itself). Both are declared in the same afb.toml and resolved by burn install.

Ruby packages ([gem])

Ruby gems from RubyGems are declared in the [gem] table in afb.toml. Each entry is gem-name = "requirement" where the requirement is a RubyGems version requirement. burn install resolves the closure, verifies each gem's SHA-256 digest, and vendors the gems into vendor/gem/** inside the .afb - the same vendoring model as [pip].

afb.toml
[gem]
sinatra = "~> 3.1"        # pessimistic: >= 3.1, < 4 (stay on 3.x)
faraday = ">= 2.7"        # at least 2.7, any patch
json    = "~> 2.7.1"      # pessimistic on patch: >= 2.7.1, < 2.8
nokogiri = ">= 0"        # any version (Bundler convention for "unconstrained")
shell
$ burn install      # resolve + vendor [gem] packages into vendor/gem/**
$ burn run          # require 'sinatra', 'faraday', etc. from the vendored gems
i
Supported requirement forms (v1).

All standard RubyGems operators are accepted: = (exact), != (exclusion), <, >, <=, >=, and ~> (pessimistic). Comma-separated conjunctions are supported (e.g. >= 2.7, < 3). ~> requires at least two version components (e.g. ~> 3.1 is valid; ~> 3 is not). The conventional >= 0 form means any version.

Not supported in v1 (refused with an error): git requirements (git:..., git@...), direct URL requirements (https://...), and local path requirements (./path, /abs/path). Use a registry requirement instead.

The .afb format

A .afb is not "plain JavaScript" - that was the old single-language story. It is a zstd-compressed ustar tar archive, content-addressed by the SHA-256 of its compressed bytes, carrying the manifest, the manifold, the source/ tree, and - optionally - a precompiled WASM module. The binding contract is in afterburner-afb/FORMAT.md.

AspectContract
Containerzstd level 19 over a ustar tar; entries sorted, mtime=0, uid=gid=0 -> byte-reproducible build.
AddressingThe filename is the SHA-256 of the compressed bytes. Same inputs, same digest.
Required membersafb.toml, manifold.json, and the source/<entry> file named by package.entry.
Optional memberA precompiled module under precompiled/wasm32-wasip1/main.wasm (self-contained) or precompiled/wasm32-wasip1-dyn/main.wasm (dynamically linked); format minor >= 2.
Versioning[format] version = "MAJOR.MINOR". One reader owns one major and refuses a different one loudly; a greater minor is accepted (additive, unknown fields ignored).
Safety boundsCompressed cap 32 MiB, decompressed 256 MiB (streamed, early abort); ≤1000 entries; .. / absolute / symlink / non-regular entries rejected.

The registry

Publish and install go through the registry (default https://registry.afterburner.sh). Publishing packs source/ + manifest + manifold (+ optional precompiled) into a .afb, content-addresses it by SHA-256, and uploads it. Installing resolves the dependency closure, writes burn.lock, fetches each member concurrently, and verifies every download's digest against the lock before unpacking into the shared cache.

shell
$ burn login                          # store an afbpat_… token
$ burn publish                        # build + upload the package in the current dir
$ burn publish nyquist-widget-0.1.0.afb  # upload a prebuilt .afb
$ burn install nyquist/widget@^0.1     # resolve + fetch into the cache
$ burn search geometry                # full-text search the registry
$ burn info nyquist/widget             # versions, capabilities, digest
$ burn yank nyquist/widget@0.1.0       # hide a version (--undo restores)

Two lockfiles exist, both TOML and content-addressed: burn.lock sits beside afb.toml and pins the project's resolved closure for reproducible installs; afb.lock records the resolved closure inside a built .afb. A built .afb always carries resolved sha256: pins, so what you tested is what ships.

Precompiled WASM packaging

burn compile packs the .afb and, for the interpreted languages, can additionally ahead-of-time compile source/ to a WASM module stored at precompiled/wasm32-wasip1/main.wasm inside the archive (the JS/TS path uses an opt-in build-time tool that is never needed at runtime). The runtime loads that module directly, skipping the per-call compile pass, so cold starts stay sub-millisecond. The compiled languages (C/C++/Rust/Go) are their WASM module by construction. The .afb retains source/ as a fallback for readers that do not consume the precompiled artifact.

shell
$ burn package                  # source-based .afb (runtime compiles on first load)
$ burn package --wasm-only      # full-WASM: precompiled only, no source in the .afb
$ burn compile                  # package + AOT precompile to precompiled/wasm32-wasip1/main.wasm
$ burn compile -o ./dist/        # write the .afb to a specific output path

The precompiled module respects the package's Manifold: a sealed package produces a self-contained module; a capability-bearing one keeps the host-import boundary so every grant stays enforced at runtime.

Building packages for ScramDB

ScramDB embeds Afterburner to run SQL user-defined functions. A package written for it follows two extra conventions on top of the ordinary package shape - both live inside features Afterburner already has, so a ScramDB package is a perfectly ordinary .afb.

Scaffold one with --scramdb (shorthand for --template scramdb), which writes both conventions for you:

shell
$ burn init ./my-udf --namespace acme --name my_function --scramdb

1. Declare the SQL signature. [metadata] is Afterburner's free-form table, never interpreted by the reader; ScramDB reads a [metadata.sql] table out of it and registers each declared function on install. Without this section a package installs and catalogs fine but binds no callable function, so declare one entry per function:

afb.toml
[[metadata.sql.function]]
name    = "fts_term_frequency"
args    = "doc TEXT, term TEXT"
returns = "INT"

The argument names are load-bearing: the body reads its inputs by name out of the batch (batch.columns.doc), so they must match the entry source exactly. With the section present, SELECT fts_term_frequency(doc, 'needle') FROM docs works immediately after install - no manual CREATE FUNCTION.

2. Take a columnar batch, not a row. ScramDB is a columnar engine and calls a UDF once per batch, so a package crosses the sandbox boundary once per chunk instead of once per row. The argument is { row_count, columns } and the return value has the same shape:

source/main.js
// One batch in, one batch out - never one row at a time.
module.exports = function (batch) {
  const doc = batch.columns.doc;      // keyed by the declared arg names
  const term = batch.columns.term;
  const n = batch.row_count;
  const out = new Int32Array(n);
  for (let i = 0; i < n; i++) out[i] = count(doc[i], term[i]);
  return { row_count: n, columns: { frequency: out } };
};

Types map to the SQL types declared in args/returns: numeric and temporal columns arrive as TypedArrays, and TEXT columns as strings. A scalar (constant) argument is transferred once for the whole batch rather than repeated per row.

Compile before you ship a per-row UDF. A source-only package is interpreted at call time, and a UDF pays that cost on every row: a SHA-256 package over 20k rows can exhaust a default fuel budget before it finishes. burn compile emits the precompiled members alongside the source; ship those for anything CPU-heavy.

3. Daemon packages: long-running servers the database starts for you. A package whose job is to listen rather than answer per-batch calls - ScramDB’s own scramdb/semantics Semantic AI MCP server (port 9191) is the first - declares itself a daemon and grants its port in the manifold:

afb.toml
[metadata.daemon]
autostart = true

On install ScramDB accepts a daemon package with zero [metadata.sql] entries (a package declaring neither is still refused loudly). After its boot preinstall sweep, ScramDB starts every daemon-declaring installed package in-process on the embedded engine: DaemonHttp::with_runtime binds the listeners onto the database’s own reactor and a DaemonShardPool owns init and dispatch - the same machinery burn server.js uses, driven by the embedder. Each daemon is supervised (bounded restart backoff) and visible in the scram_daemons catalog view; env reaches it through the operator’s [udf.daemon] env_allowlist, and the package’s own manifold still gates everything it touches.

#Programmatic API (embed in Rust)

Afterburner is also a Rust library: embed the engine to run untrusted code inside your own application. Add the crate and you have a sandboxed runtime in a handful of lines - register a script, hand it JSON, get JSON back. The default mode is adaptive (fast cold start, then the sandboxed path); the default Manifold is sealed.

Cargo.toml
[dependencies]
afterburner = { version = "0.2", features = ["wasm", "native", "adaptive"] }
serde_json = "1"
rust
use afterburner::Afterburner;
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ab = Afterburner::new()?;                              // adaptive engine, sealed manifold
    let id = ab.register("module.exports = (d) => d.n + 1")?;   // compile + cache by content hash
    let out = ab.run(&id, &json!({ "n": 41 }))?;
    assert_eq!(out, json!(42));
    Ok(())
}

Configure capabilities, fuel, timeouts, and state on the builder. A precompiled WASM module can be registered directly with register_precompiled(wasm, target); bulk payloads have a bytes-in fast path (run_raw) and a typed columnar UDF path (run_columnar).

rust
use afterburner::{Afterburner, Manifold, FsAccess, NetAccess};

let ab = Afterburner::builder()
    .manifold(Manifold {
        fs:  FsAccess::ReadWrite(vec!["/var/data".into()]),
        net: NetAccess::OutboundHttp(Some(vec!["api.example.com".into()])),
        crypto: true,
        ..Manifold::sealed()
    })
    .fuel(your_op_budget)
    .timeout_ms(your_deadline)
    .build()?;

#Embedder extension points

Pluggable seams on the Rust side, each with a sensible default. Swap your own when the default is too tight.

BurnCacheBackend

Compiled scripts are content-addressed by SHA-256 of the source. Implement BurnCacheBackend to share that cache across processes or hosts (Redis, S3, SQLite, NATS). The default InProcessCacheBackend is a lock-free map per Afterburner instance. See examples/cache-backend-sqlite for a SQLite-WAL coordinator two instances share scripts through.

rust
use afterburner::{Afterburner, BurnCacheBackend, Result};
use std::sync::Arc;

struct RedisBackend { client: redis::Client }
impl BurnCacheBackend for RedisBackend {
    fn fetch(&self, hash: &[u8; 32]) -> Result<Option<String>> { /* GET */ }
    fn publish(&self, hash: &[u8; 32], source: &str) -> Result<()> { /* SETEX */ }
}

let ab = Afterburner::builder()
    .cache_backend(Arc::new(RedisBackend { client }))
    .build()?;

HostContext

Custom callbacks scripts reach via require('afterburner:host'). Useful for streaming UDFs (emit_row), structured logging (log), and reading host-resolved configuration (get_env) without granting raw process.env.

rust
use afterburner::{Afterburner, HostContext, LogLevel};
use serde_json::Value;
use std::sync::Arc;

struct AppHost { config: AppConfig, sink: Sender<Value> }
impl HostContext for AppHost {
    fn log(&self, level: LogLevel, msg: &str) { tracing::info!(?level, msg); }
    fn get_env(&self, key: &str) -> Option<String> { self.config.resolve(key) }
    fn emit_row(&self, row: Value) { let _ = self.sink.try_send(row); }
}

let ab = Afterburner::builder()
    .host_context(Arc::new(AppHost::new(cfg, tx)))
    .build()?;

FlowEngine (multi-module bundles)

For pipelines that span multiple ES modules. Enable the flow feature, call .flow() on the builder, then register_bundle(entry, modules) to compile a graph of files at once.

rust
let ab = Afterburner::builder().flow().build()?;
let id = ab.register_bundle("./pipeline.mjs", &[
    ("./normalize.mjs".into(), normalize_src),
    ("./enrich.mjs".into(),    enrich_src),
])?;
let out = ab.run(&id, &trigger)?;
i
StateStore is the fourth seam.

Covered in Cross-invocation state. The default InMemoryStateStore is lock-free with atomic counters; swap your own to back require('afterburner:state') with Redis, SQLite, or anything else.

#The burn CLI

A Deno-style command-line runtime. The CLI default is open (so existing Node scripts drop in without flags); pass --sandbox to flip to deny-by-default and grant with --allow-*. Every subcommand is below.

Subcommands

SubcommandPurpose
Execute
run [file] [args]Compile (by file extension) and execute. With no file, runs the current package's entry from afb.toml. Args after the file populate the program's argv. The default when the first positional looks like a path.
eval <code> · -eEvaluate an inline JavaScript snippet.
repl [--lang L]Interactive REPL for any supported language. --lang default js; see The REPL.
thrust <file>UDF mode (JavaScript). Stdin JSON in, the script returns JSON, written to stdout. Body is module.exports = (data) => ....
check <file>Compile + parse without executing. Exit 0 on success, 1 on error. Useful in CI.
bench <file>Throughput + p50 / p99 latency. --iters N (default 10000), --workers N (0 = auto).
Build & package (see Packages)
new <ns/name> · init [dir]Scaffold a package. --lang L picks the language (default js); --ts is shorthand for --lang typescript; --template module|udf|http|llm.
add <ns/name>Add a registry package to [dependencies], pinned by digest. (Edit [npm] directly for npm deps.)
install [pkg]Resolve + cache the dependency graph and write burn.lock. With no pkg, installs the current package's deps. --locked reuses the lockfile; --jobs N sets concurrency.
test [dir]Run every file under tests/ through the sandbox.
package [dir] · -oBuild a deterministic, content-addressed .afb. --compile precompiles JS/TS to WASM; --wasm-only emits a WASM-only archive with no source.
compile [dir] · -oLike package, but also AOT-compiles to a WASM module under precompiled/ so the runtime loads it directly. Honors the package's Manifold; keeps source/ as a fallback.
publish [afb]Build (or upload a prebuilt .afb) to the registry. --registry, --token, --compile / --no-compile.
clean [dir]Remove build artifacts. --cache also clears the shared caches under ~/.cache/burn.
Registry
login · logout · whoamiRegistry authentication (stores / removes an afbpat_… token; --registry selects a named registry).
search <q> · info <pkg>Full-text search the registry; show a package or version's metadata (versions, capabilities, digest).
yank <ns/name@ver>Hide a published version from resolution. --undo restores it.
Tooling & pass-through
agent <action>Wire AI coding assistants to run JavaScript sealed. See AI coding agents.
node | npm | pnpm | npx | yarn | bunPass-through shim. Forwards args to the ecosystem binary on $PATH; child node processes route back through burn.
versionPrint the version and the cargo features built into the binary.

Flag reference

These global flags apply to run / eval and the other execution paths.

FlagEffect
-e <code> · --eval <code>Run an inline JavaScript snippet instead of a file.
--mode native|wasm|adaptiveEngine backend. Defaults to adaptive.
--fuel <N>Per-call instruction budget (backend-specific; not portable across modes).
--memory <BYTES>Per-call linear-memory cap.
--timeout <MS>Per-call wall-clock cap. The portable safety knob.
--sandboxSeal the manifold (deny everything); combine with --allow-* to opt in.
--allow-net <hosts>Comma list of hostnames. * = any host; wildcards (*.trusted.io); optional :port pins one port.
--allow-listen <ports>Inbound listening. * = any; comma list; or a 9000-9100 range.
--allow-fs <roots>Comma list of read+write FS roots. * = all paths.
--allow-fs-read <roots>Read-only FS roots (same shape as --allow-fs).
--allow-fs-write <roots>Write FS roots.
--allow-env <keys>Comma list of env-var keys. * = every variable.
--allow-cryptoGrant the crypto capability inside an explicit sandbox.
-A · --allow-allGrant every capability. Trusted scripts only.
--env-file <path>Load KEY=VALUE lines into the environment before the script runs (repeatable).
--watchRe-run when the file or a local require() changes on disk.
-q · --quietSuppress banners, capability hints, and warnings on stderr.

afb.toml keys

Section · keyRequiredMeaning
[format] versionYes"MAJOR.MINOR". A different major is refused; a greater minor is accepted.
[format] min_readerNoReject if the reader is older than this version.
[package] namespaceYesRegistry namespace (non-empty).
[package] nameYesPackage name (non-empty); published as namespace/name.
[package] versionYesSemver, validated.
[package] languageYesOne of the eight (with aliases): js, javascript, ts, typescript, rust, go, golang, c, cpp, c++, cxx, cc, python, py, ruby, rb.
[package] entryYesEntry path; must live under source/ and not escape it.
[package] description · homepage · licenseNoDescriptive; description is search-indexed; license is an SPDX id.
[package] keywordsNoFree-form search keywords (array).
[runtime] minYesMinimum afterburner-core version (semver).
[runtime] targetNoPrecompiled module target: wasm32-wasip1 or wasm32-wasip1-dyn (format minor >= 2).
[dependencies]No"ns/name" = a semver range, a sha256: pin, or a { path } / { git, tag|branch|rev } table.
[npm]No"pkg" = "^range"; vendored into source/node_modules/** by burn install.
[pip]No"pkg" = "specifier"; PEP 440 registry specifiers only (no git/URL/path/markers/extras in v1). burn install resolves + vendors wheels into vendor/pip/** inside the .afb. See Python packages.
[gem]No"gem" = "requirement"; RubyGems registry requirements only (no git/URL/path in v1). burn install resolves + vendors gems into vendor/gem/** inside the .afb. See Ruby packages.
[metadata]NoFree-form table the reader never interprets; round-trips verbatim.

#AI coding agents

One command makes your AI assistant run every piece of JavaScript it writes sealed inside the sandbox instead of raw node. Supported: Claude Code, OpenAI Codex, Gemini CLI, Cursor, GitHub Copilot, Antigravity - pick any combination in one arrow-key multi-select.

shell
$ burn agent install                 # multi-select; detected assistants pre-checked
$ burn agent install --host claude-code --host gemini   # scripted, no picker
$ burn agent status                  # what's detected, wired, and current
$ burn agent disable                 # pause the redirect (hooks stay wired)
$ burn agent uninstall --all         # remove from every assistant

This wires a pre-tool hook into each assistant's config and adds a short instruction block to its memory file. When the assistant tries node app.js, npm test, or npx tsx ..., the hook hands back the corrected command - burn --sandbox node app.js - and the assistant re-runs it sealed: no network, no filesystem, no env. When a capability is genuinely required it is granted narrowly per run (burn --sandbox --allow-net=api.example.com node app.js). The hook itself is a microsecond string check - no engine, no network - so the assistant stays fast. Uninstall is the exact inverse: hook entries and instruction blocks are spliced out and everything else is preserved byte-for-byte.

#Node compat surface

For JavaScript and TypeScript, Afterburner targets the Node.js 26 surface. burn npm install and require('express') from a real node_modules tree both work end to end - see examples/express-app. Pure-JS modules are always available; host-backed ones are gated by the Manifold.

Always-on (pure JS)

path
url
querystring
events
assert
buffer
util
stream
string_decoder
timers
process
console
os
zlib
perf_hooks
async_hooks
vm

Host-backed (capability-gated)

fs
fs/promises
crypto
http
https
http2
net
tls
dns
dgram
child_process
worker_threads
cluster
node:test
node:sqlite

Web globals

fetch
Request / Response
Headers
AbortController
URL
URLSearchParams
TextEncoder / Decoder
structuredClone
performance.now
WebAssembly

Sandbox security omissions

A few Node APIs are refused by design so user code cannot escape the capability sandbox. Each throws a Node-shaped error so existing code fails fast.

APIReason
process.chdirWould slip paths past the Manifold fs allow-list.
process.setuid / setgid / seteuid / setegidIdentity changes belong to the host, not sandboxed code.
new Worker(code, { eval: true })String-as-code bypasses the fs allow-list. Use new Worker(path).
Native .node addonsBypasses the sandbox. Use the pure-Rust shadows (bcrypt, argon2, jsonwebtoken, sqlite3, sharp).

#Environment variables

VariableDefaultPurpose
AFTERBURNER_LOGwarnLevel: off · error · warn · info · debug · trace
AFTERBURNER_LOG_FORMATtextReporter: text (stderr) or json (stdout NDJSON)
BURN_PYTHON_RUNTIMEunsetOptional developer override: point at a local Python runtime directory instead of the auto-fetched one. Not required for normal use; the runtime is auto-fetched or embedded in the release binary.
BURN_MAX_LINEAR_MEMORY1GPer-instance WASM linear-memory ceiling (pooling allocator). Plain bytes or a K/M/G suffix; clamped to 4 GiB (wasm32 ABI limit). Raise for long-running daemons; lower for many concurrent instances.
BURN_SHARDSauto (CPU count)Parallel workers the HTTP daemon uses. Accepted range 1 to 128; out-of-range or non-numeric falls back to auto with a warning. BURN_SHARDS=1 reverts to single-worker.
BURN_QUIETunsetSet to 1 to suppress the first-run banner and non-essential stderr notices. Same as --quiet.
BURN_VERSIONlatestVersion pin used by the bootstrap installers (e.g. v0.1.3). No effect after install.
BURN_INSTALL~/.local/binInstall dir for the bootstrap installers. The runtime does not read this.
BURN_INSTALL_NO_PATHunsetSet to 1 to skip the bootstrap installer's automatic $PATH update.

#Workspace crates

Eleven crates layered for replaceability. Drop down a level whenever the facade is too tight.

CratePurpose
afterburnerFacade: Afterburner + builder, the burn binary, one ergonomic entry point and CLI.
afterburner-coreCombustor trait, Manifold, FuelGauge, BurnCache, BurnCacheBackend, StateStore, HostContext, level-gated logging.
afterburner-afbThe .afb format: pack / unpack, the manifest parser, the dependency resolver and afb.lock.
afterburner-cloudRegistry client: publish / install / yank, project burn.lock, concurrent digest-verified fetch.
afterburner-wasiThe WASM sandbox + the deterministic engine. Host-function imports, pooling allocator, bytecode cache, the Python runner and native-extension linking (numpy / pandas), daemon coordinators.
afterburner-igniteNative JS engine; thread-local runtimes for sub-millisecond ignition.
afterburner-node-compatNode polyfill bundle + Rust-backed host implementations. Bounded HTTP & DNS; pure-Rust shadows for bcrypt, argon2, jsonwebtoken, sqlite3, sharp.
afterburner-flowFlowEngine::load / execute / unload + register_bundle for ES-module bundles.
afterburner-adaptiveFlying Start: native-to-WASM tier switch on the second call.
afterburner-thrustMulti-threaded scheduler. Bounded per-worker deques, token-bucket admission, steal-when-idle, graceful drain.
afterburner-pluginWASM-side runtime plugin, baked into the binary and pre-initialised for fast ignition.

#Building from source

The prebuilt burn binary needs nothing external to run JS/TS/Python. To compile the workspace yourself you need a recent Rust toolchain (2024 edition, MSRV 1.90+) and the system C toolchain. Compiling a guest in a native language additionally needs that language's toolchain (cargo, go, clang + a WASI sysroot).

shell
$ git clone https://github.com/afterburner-sh/afterburner
$ cd afterburner
$ cargo build --release --features bin
$ ./target/release/burn version
i
Reproducible by design.

All execution-engine artifacts are committed in-tree and pulled via include_bytes!. cargo build is fully offline once dependencies are resolved.