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 (undefinedwhen none was given).console—log,info,debugto stdout;warn,errorto stderr.fs—readFileSync,writeFileSync,readdirSync,statSync,existsSync,mkdirSync,unlinkSync, plus anfs.promisesfacade 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, plussepanddelimiter.process—env,argv,cwd(),platform,pid,version.fetch— only with--allow-net. A modern subset:fetch,Headers, andResponsewithtext(),json(), andarrayBuffer(). NoXMLHttpRequest, 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
fetchbody 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-netloopback, private, and link-local addresses require--allow-private(link-local never).
Where It Runs
- Extension tiles — the server spawns
suwu gqper 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.