JavaScript and TypeScript SDK
@faber/runtime-sdk is the TypeScript and JavaScript client for the Faber
API v1. It covers every operation, builds requests fluently, runs
client-side tests on results, and reports failures through one error type.
It uses only fetch, so it runs in Node.js 18+, Bun, Deno, browsers and edge
runtimes.
The 0.3 SDK requires a Faber 0.3 service. Upgrading from 0.2 is covered in the migration guide.
Install
Section titled “Install”npm install @faber/runtime-sdkWith other tools: bun add @faber/runtime-sdk, pnpm add @faber/runtime-sdk,
yarn add @faber/runtime-sdk. In Deno, import from npm:@faber/runtime-sdk.
Configure
Section titled “Configure”import { FaberClient } from '@faber/runtime-sdk';
const client = new FaberClient({ baseUrl: 'http://localhost:3000', apiKey: process.env.FABER_API_KEY,});| Option | Default | Meaning |
|---|---|---|
baseUrl | required | Service URL, with or without /api/v1. Relative URLs work in browsers. |
apiKey | none | Sent as Authorization: Bearer <key>. |
timeoutMs | 60000 | Deadline for a whole call, retries and waits included. 0 disables it. |
maxRetries | 2 | Retries after a 503, a 429 or a network error. |
retryDelayMs | 500 | First backoff delay; doubles per retry, with jitter. |
maxRetryDelayMs | 30000 | Longest wait before a retry. A longer Retry-After ends the retries. |
headers | {} | Extra headers on every request. |
fetch | global fetch | Any fetch-compatible function. |
Every method takes an optional last argument { signal, timeoutMs, maxRetries, headers }
that applies to that call only:
const controller = new AbortController();const info = await client.info({ signal: controller.signal, timeoutMs: 5_000 });Run tasks
Section titled “Run tasks”A request is a list of steps run in order in one sandbox workspace,
/faber. A step is a single task, or an array of tasks that run in parallel.
Files written by one step are visible to the later ones.
execute
Section titled “execute”execute(request) sends POST /execute and returns { steps, cache }:
const response = await client.execute({ stop_on_failure: true, steps: [ { cmd: 'gcc', args: ['-O2', '-o', 'add', 'add.c'], files: { 'add.c': source }, cache: true }, [ { cmd: './add', stdin: '1 2\n', sandbox_profile: 'native_v2' }, { cmd: './add', stdin: '40 2\n', sandbox_profile: 'native_v2' }, ], ],});
response.steps[0]; // the compile resultresponse.steps[1]; // an array: one result per parallel taskresponse.cache; // { enabled, hits, stored }Request fields:
| Field | Meaning |
|---|---|
steps | Tasks and parallel task arrays |
cache | Default for tasks that do not set cache (default false) |
stop_on_failure | After a step where a task did not exit 0, report the remaining steps as not started (default false) |
Task fields:
| Field | Meaning |
|---|---|
cmd | Program; resolved through PATH unless it contains / |
args, env, stdin | Arguments, environment, standard input |
files | Workspace-relative path to file content (see below) |
working_dir | Absolute start directory (default /faber) |
sandbox_profile | compile_v2, native_v2, compile_v1 or native_v1; the service default otherwise |
cache | The task is deterministic and its step may be cached |
A file is a string (UTF-8 text), an inline file
{ content, encoding: 'utf8' | 'base64', executable }, or a reference to an
uploaded file { file_id, executable }.
executeSingle
Section titled “executeSingle”Runs one task and returns its result:
const result = await client.executeSingle({ cmd: 'echo', args: ['hello'] });Results
Section titled “Results”Each step result has the shape of its step. A task result is either completed or failed:
type TaskResult = | { stdout: string; stderr: string; exit_code: number; stats: TaskStats; cached: boolean } | { error: string; stats: TaskStats; cached: boolean };A completed task ran and reported an exit status, which may be non-zero
(128 + n when killed by signal n). A failed task never reached one: the
command was not found, setup failed, or it was skipped. stats holds
memory_peak_bytes, cpu_usage_usec, pids_peak, execution_time_ms,
stdout_truncated, stderr_truncated, termination_signal and the
outcome:
| Outcome | Meaning |
|---|---|
exited | Exited on its own |
signaled | Killed by a signal |
timed_out | Hit the wall-clock or CPU time limit |
out_of_memory | Killed by its memory limit |
ancestor_out_of_memory | Killed by a limit above the task’s own |
pids_limit | Hit the process limit |
output_limit | Produced more output than allowed |
policy_violation | Made a syscall its sandbox profile forbids (SIGSYS) |
not_started | Never started (stop_on_failure, or the overall deadline passed) |
infrastructure_failure | Faber could not run it |
These are results, not exceptions. Helpers read them:
import { allSucceeded, firstFailure, taskResults, isCompleted, isSuccess, isSkipped } from '@faber/runtime-sdk';
if (!allSucceeded(response)) { const { stepIndex, taskIndex, result } = firstFailure(response)!; console.log(stepIndex, taskIndex, result.stats.outcome);}
for (const { stepIndex, taskIndex, parallel, result } of taskResults(response)) { if (isCompleted(result)) console.log(stepIndex, taskIndex, result.exit_code);}isSuccess means completed with exit status 0; isSkipped means outcome
not_started.
Build requests
Section titled “Build requests”TaskBuilder collects steps and the request options; task() builds one
task. Both produce the plain objects above.
import { TaskBuilder, task } from '@faber/runtime-sdk';
const plan = new TaskBuilder() .cache(false) .stopOnFailure() .single( task('gcc', ['-O2', '-o', 'main', 'main.c']) .file('main.c', source) .file('data/blob.bin', new Uint8Array([0, 1, 2])) .executable('run.sh', '#!/bin/sh\nexec ./main\n') .fileRef('input.txt', fileId) .cache(), ) .parallel([ task('./main').stdin('1 2\n').profile('native_v2'), task('./main').stdin('40 2\n').profile('native_v2').env({ LANG: 'C' }), ]) .single(task('sh', ['-c', 'exit 3']).workingDir('/faber/data'));
const response = await client.execute(plan);plan.toRequest(); // the request object it sendsTaskBuilder | |
|---|---|
single(task) | Adds a one-task step (a plain task or a task() spec) |
parallel(tasks) | Adds a parallel step |
cache(enabled = true) | Sets the request-level cache default |
stopOnFailure(enabled = true) | Sets stop_on_failure |
singleWithTests(task, tests), parallelWithTests(tasks) | Steps with client-side tests |
build(), toRequest() | The steps; the full request |
task(cmd, args?) | |
|---|---|
args(...args), env(vars), stdin(text) | Arguments (appended), environment (merged), standard input |
file(path, content, { executable }) | Text, bytes (sent as base64), or an inline file or file reference object |
executable(path, content) | A file with mode 0755 |
fileRef(path, fileId, { executable }) | An uploaded file |
workingDir(dir), profile(name), cache(enabled = true) | Start directory, sandbox profile, cacheable |
test(...tests), build() | Client-side tests; the task object |
For plain objects, textFile(content, opts), binaryFile(bytes, opts),
fileRef(id, opts) and file(content, opts) return file values, and
encodeBase64(bytes) is available too.
Step cache
Section titled “Step cache”When the service runs with CACHE_ENABLED=true, a step whose tasks all have
cache: true is stored after every task in it exits 0, together with the
workspace it leaves behind. The next identical request replays it: the result
comes back with cached: true and the original stats, and later steps see
the restored files. Only a leading run of cacheable steps is cached, so mark
deterministic setup (compiling, installing dependencies) and leave steps that
run untrusted input uncached. response.cache.hits and stored count the
replayed and newly stored steps; /tmp is not part of a cached layer.
const upload = await client.uploadFile(bytes, { filename: 'input.txt', contentType: 'text/plain' });// { file_id, size, already_exists }
const { files } = await client.listFiles();// [{ id, filename, content_type, size, created_at: '2026-10-18T12:00:00Z' }]
const content = await client.downloadFile(upload.file_id); // Uint8Arrayawait client.deleteFile(upload.file_id);uploadFile accepts a Blob or File, Uint8Array (including Node
Buffer), ArrayBuffer, ReadableStream (read fully before sending) or a
string (UTF-8). The filename and content type are sent only when you pass
them or the data is a File. Files are stored under the SHA-256 of their
content; uploading the same bytes again returns the same id with
already_exists: true. Use the id in a task with fileRef.
In Node.js, Bun and Deno, read from disk with the separate /node entry:
import { uploadFileFromPath } from '@faber/runtime-sdk/node';
const { file_id } = await uploadFileFromPath(client, './testdata/input.txt');Service operations
Section titled “Service operations”| Method | Endpoint | Returns |
|---|---|---|
health() | GET /health | { status: 'ok' | 'shutting_down', version }; a draining service answers 503, which resolves rather than throws |
info() | GET /info | version, arch, sandbox_profiles { default, allowed }, limits, cache { enabled, backend }, toolchains |
clearCache() | DELETE /cache | { cleared }; throws cache_disabled when the service has no cache |
metrics() | GET /metrics | Prometheus text |
const info = await client.info();console.log(info.limits.wall_timeout_ms, info.sandbox_profiles.allowed);Client-side tests
Section titled “Client-side tests”Attach tests to tasks, run them with executeWithTests, and summarize them
with TestResultAnalyzer. Tests are removed from the request before it is
sent.
import { TaskBuilder, TestResultAnalyzer, task } from '@faber/runtime-sdk';
const cases = [ { input: '1 2\n', expected: '3\n' }, { input: '40 2\n', expected: '42\n' },];
const plan = new TaskBuilder() .stopOnFailure() .single( task('gcc', ['-O2', '-o', 'add', 'add.c']).file('add.c', source).cache() .test({ name: 'compiles', assertion: 'equals', field: 'exit_code', expected: 0 }), ) .parallel( cases.map((c, i) => task('./add').stdin(c.input).profile('native_v2') .test({ name: `case ${i + 1}`, assertion: 'equals', field: 'stdout', expected: c.expected }), ), );
const result = await client.executeWithTests(plan);const analyzer = new TestResultAnalyzer(result);console.log(analyzer.formatReport());analyzer.assertAllPassed();| Assertion | Fields | expected |
|---|---|---|
equals | stdout, stderr, exit_code | string or number |
contains | stdout, stderr | string |
matches | stdout, stderr | RegExp |
custom | testFn: (result) => ({ name, passed, message }) |
executeWithTests returns the raw results, the cache summary,
stepResults (each with passed, skipped and cached),
allTestsPassed, passedCount and failedCount. A task that never reached
an exit status fails with a task execution test. TestResultAnalyzer
provides allPassed, totalSteps, passedSteps, failedSteps,
skippedSteps, cachedSteps, getFailedSteps(), getFirstFailure(),
getStageFailure(index), getStageSummary(),
formatReport({ showPassed, includeOutput }) and assertAllPassed().
Skipped and cached stages are marked in the report.
Errors
Section titled “Errors”Every failed call throws a FaberError:
import { FaberError } from '@faber/runtime-sdk';
try { await client.execute(plan);} catch (error) { if (!(error instanceof FaberError)) throw error; switch (error.code) { case 'unauthorized': // check FABER_API_KEY break; case 'capacity_exhausted': case 'shutting_down': case 'rate_limited': // already retried; try later (error.retryAfterMs) break; default: console.error(error.status, error.code, error.message, error.requestId); }}| Field | Meaning |
|---|---|
status | HTTP status; 0 when no response arrived |
code | The API’s error code, or a client code |
message | The server’s message, or a description |
requestId | The x-request-id response header |
retryAfterMs | The server’s Retry-After, in milliseconds |
rawCode | The server’s code when the SDK does not know it (code is then internal_error) |
body, cause | Parsed error body; underlying error |
retryable | true for 503, 429 and network errors |
| Code | Status | When |
|---|---|---|
invalid_request | 400 | Malformed body or parameters |
unauthorized | 401 | Missing or wrong API key |
not_found | 404 | No such file |
file_not_found | 404 | A task references a file id that is not stored |
payload_too_large | 413 | Body over the configured limit |
limits_exceeded | 422 | Too many steps or parallel tasks |
profile_not_allowed | 422 | Sandbox profile not allowed |
cache_disabled | 409 | clearCache() without a cache |
rate_limited | 429 | The API key used up its request allowance |
capacity_exhausted | 503 | No free execution slots |
shutting_down | 503 | The service is draining |
cancelled | 503 | The request was cancelled |
deadline_exceeded | 504 | The request overran its deadline |
sandbox_setup_failed | 500 | The sandbox could not be set up |
internal_error | 500 | Anything else |
network_error | 0 | The connection failed |
timeout | 0 | timeoutMs passed |
aborted | 0 | The caller’s AbortSignal fired |
invalid_response | any | A success response had an unexpected shape |
Retries. 503 and 429 responses and network errors are retried up to
maxRetries times. The client waits for Retry-After when the server sends
it, and otherwise backs off exponentially with jitter. Every other 4xx
response, other 5xx responses and timeouts are thrown at once. A retry that would not
fit within timeoutMs, or a Retry-After longer than maxRetryDelayMs,
ends the retries with the last error.
ERROR_CODES, TASK_OUTCOMES and SANDBOX_PROFILES are exported as arrays
for building UIs or validating input.
Runtimes and bundles
Section titled “Runtimes and bundles”| Runtime | Use |
|---|---|
| Node.js 18+ | import (ESM) or require (CommonJS) |
| Bun, Deno | ESM; the /node entry works too |
| Browsers, edge | ESM, or dist/index.global.js in a script tag (window.Faber) |
In a browser, keep the API key on a server you control and call Faber through it, or put Faber behind a proxy that adds the key.
Examples
Section titled “Examples”The SDK ships five runnable examples in
sdks/js/examples:
| Example | Shows |
|---|---|
hello-world.ts | Health check and one command |
compile-and-run-c.ts | Compile C, run it with stdin, report the first failure |
cached-test-cases.ts | A cached compile step and parallel test cases with a test report |
upload-file.ts | Upload a file, list it, use it by reference, download and delete it |
errors-and-limits.ts | FaberError codes; timeout, out-of-memory and policy-violation outcomes |
cd sdks/jsnpm ciFABER_URL=http://localhost:3000 FABER_API_KEY=your-key npx tsx examples/cached-test-cases.ts