gqjs Script Runner

suwu gq runs one JavaScript file in a request-scoped QuickJS sandbox: a fresh isolate, scoped filesystem roots, an optional input payload, and a hard deadline. The same runtime that renders extension tiles.

Overview

suwu gq runs a single JavaScript file in a request-scoped QuickJS sandbox and prints what it produced. Each run gets a fresh isolate, an optional JSON input, capability-scoped filesystem roots, and a hard deadline — then the isolate is destroyed. There is no event loop and nothing is shared between runs.

It is the runtime behind extension tiles (there, one script per HTTP request, in its own process) and it is a perfectly good command-line tool on its own: a small JSON transformer, a file-report generator, a health probe.

QuickJS runs on WebAssembly via wazero, so there is no CGO, no Node, and no system dependency — the whole engine ships inside the suwu binary.

Usage

suwu gq [flags] <script.js>
Flag Description
--input <json> Input payload, passed to the script as the global input.
--input-file <file> Read the input payload from a file instead of the command line.
--root <virtual=real> Mount a host directory inside the sandbox. Repeatable, e.g. /app=./src.
--ro Make the filesystem read-only.
--cwd <dir> Virtual working directory (default /).
--env <KEY=VALUE> Set an environment variable. Repeatable.
--arg <value> Append an entry to process.argv. Repeatable.
--timeout <dur> Wall-clock deadline for the whole run (default 5s).
--allow-net Enable fetch() for http/https targets.
--allow-private With --allow-net, also allow loopback and private addresses. Link-local addresses stay blocked.
--print-result Print the script's result as JSON on stdout.
--result-file <path> Write the JSON result to a file, for machine consumption.

Exit Codes

The exit status is meaningful, so suwu gq composes with shell scripts and CI:

Code Meaning
0 Success.
1 Script or runtime error.
2 Timed out.
3 Output limit exceeded.
64 Usage error.

Script Contract

If the script defines a global function named handler, it is called with the input value and its return value becomes the result. Otherwise the script's last expression is the result:

async function handler(input) {
  const text = await fs.promises.readFile("/app/data.txt");
  return { n: input.n, doubled: input.n * 2, text };
}

Available globals:

  • input — the JSON payload (undefined when none was given).
  • console — log, info, debug to stdout; warn, error to stderr.
  • fs — readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, unlinkSync, plus an fs.promises facade over the same calls. Paths are resolved against the mounted roots; anything outside them is denied.
  • path — the usual POSIX helpers: join, basename, dirname, extname, normalize, isAbsolute, resolve, relative, plus sep and delimiter.
  • process — env, argv, cwd(), platform, pid, version.
  • fetch — only with --allow-net. A modern subset: fetch, Headers, and Response with text(), json(), and arrayBuffer(). No XMLHttpRequest, no WebSocket, no streaming bodies, no cookie jar.

Examples

suwu gq takes a script path, so the only thing you need is a file. A script that defines a global handler returns that function's value; without one, the value of the script's last expression is the result.

Transform input into JSON

cat > /tmp/double.js <<'EOF'
function handler(input) {
  return { n: input.n, doubled: input.n * 2 };
}
EOF

suwu gq --input '{"n":21}' --print-result /tmp/double.js
# {"n":21,"doubled":42}

Read a file from a mounted root

Nothing is visible until you mount it, and the guest path is the virtual one:

cat > /tmp/size.js <<'EOF'
async function handler() {
  const stat = await fs.promises.stat("/app/data.txt");
  return { size: stat.size, mtimeMs: stat.mtimeMs };
}
EOF

suwu gq --root /app=./data --print-result /tmp/size.js

Call an API

The network is off unless you ask for it, and --allow-private is a separate decision:

cat > /tmp/ping.js <<'EOF'
const res = await fetch("https://example.com/", { headers: { "X-Suwu": "gqjs" } });
({ status: res.status, contentType: res.headers.get("content-type") });
EOF

suwu gq --allow-net --print-result /tmp/ping.js
suwu gq --allow-net --allow-private --print-result /tmp/ping.js   # loopback too

Feed another program

suwu gq --result-file /tmp/out.json --input '{"n":21}' /tmp/double.js && cat /tmp/out.json

Top-level await works, so a script can be a sequence of host calls without a handler wrapper.

Limits

  • Time — 5 seconds by default; a script that overruns is killed and the run exits 2.
  • Memory — a 64 MiB QuickJS heap, 1 MiB stack.
  • Output — captured stdout and stderr are capped at 1 MiB in total.
  • Response bodies — a single fetch body is capped at 4 MiB.
  • Filesystem — nothing is visible until you mount it with --root, and traversal outside a root is refused.
  • Network — off by default, and even with --allow-net loopback, private, and link-local addresses require --allow-private (link-local never).

Where It Runs

  • Extension tiles — the server spawns suwu gq per request with a read-only root, a 5-second deadline, and a concurrency cap, then renders the returned HTML.
  • WebGPU backgrounds — the embedded WGSL compiler is the same QuickJS engine, exposed as suwu background build.