TypeScript
Run a command in a new sandbox from Node:
npm install useshardsexport SHARD_REMOTE=https://shard.example.comexport SHARD_API_KEY='your-api-key'import { Shard } from "useshards";
const shard = new Shard();try { const sandbox = await shard.create({ image: "alpine:3.20", name: "demo", memoryMiB: 512 }); try { const result = await sandbox.exec(["echo", "hello"]); console.log(result.exitCode, result.stdout.trim()); } finally { await sandbox.remove({ force: true }); }} finally { shard.close();}Replace https://shard.example.com with your server URL and your-api-key with your API key. Then run node demo.mjs. It prints 0 hello.
Install
Section titled “Install”npm install useshardsThe package needs Node 20.3 or later. It is an ES module, so load it with import. It ships its own type definitions, so the code on this page type-checks in a TypeScript project without changes. Its only runtime dependency is openapi-fetch.
Connect
Section titled “Connect”The SDK talks to shard serve on your server, never to the daemon’s local socket, and it ignores the connection that shard setup saves. It reads three settings. A non-empty remote or caFile option takes precedence over its environment variable. An apiKey option takes precedence even when it is empty, which the SDK refuses:
| Option | Variable | Value |
|---|---|---|
remote |
SHARD_REMOTE |
The URL of shard serve, such as https://shard.example.com, with only a scheme, a host and a port. |
apiKey |
SHARD_API_KEY |
The API key. |
caFile |
SHARD_CA_FILE |
A PEM file of the CA that signed the server’s certificate. It applies to an https remote only. Without it, the SDK uses Node’s default trusted CAs. |
const shard = new Shard({ remote: "https://shard.example.com", apiKey: process.env.MY_SHARD_KEY });If a setting is missing or refused, new Shard() throws ConfigurationError, and the error message never shows the key. An http remote works, but it sends the key unencrypted, so Node prints a warning. An http remote also refuses a CA file.
To mint an API key, run this on the server, with sudo on Linux:
sudo shard tokens mint --name build-agent --duration 24hThe command prints a JSON record, and its token value is the API key. Without --scopes, the key can do everything.
Call shard.close() when you are done. It releases the pooled connections so that the process can exit.
Sandboxes
Section titled “Sandboxes”shard.create() makes a sandbox and waits until it is running, which can take minutes while the server pulls the image. It starts no command of its own. It returns a Sandbox in state running. When the create fails, it throws an error, or it returns a Sandbox in state failed with the reason in sandbox.info.failedReason.
| Option | Value |
|---|---|
image |
The image, such as alpine:3.20. |
snapshot |
A snapshot ID or name. The sandbox starts from the snapshot’s files. Give image or snapshot, not both. |
name |
A name to use in place of the ID. |
env |
The environment that every command in the sandbox starts with. |
workdir |
The working directory of every command. |
user |
A user name or uid in the sandbox, such as nobody or 1000. |
secrets |
The names of the secrets to grant. |
policy |
The name of the network policy. |
memoryMiB |
The memory bound. Without it, the sandbox takes the snapshot’s own bound. From an image, it gets 512 MiB on Firecracker and vz, and no bound on gVisor, runc and Sysbox. |
vcpus, diskMiB |
The CPU count and the disk size. Without them, the daemon’s defaults apply. A sandbox from a snapshot takes the snapshot’s disk size. |
swapMiB |
The swap file, which comes out of diskMiB. Without it, the sandbox gets 2048 MiB on Firecracker and vz. gVisor, runc and Sysbox refuse any value but 0. |
ports |
The host ports to forward from the sandbox’s first start, as in Ports. |
On Firecracker and vz, a memoryMiB from 1 to 127 is refused.
shard.get(ref) finds a sandbox by ID or name. shard.list() returns { sandboxes, warnings } for the active sandboxes, and shard.list({ all: true }) adds the stopped ones. The warnings name entries that the daemon could not read, so check them before you treat the list as complete.
A Sandbox has these lifecycle methods:
| Method | What it does |
|---|---|
stop() |
Stops the sandbox and keeps its files. |
start() |
Starts a stopped sandbox with its files. |
pause() |
Saves the sandbox’s memory and files to disk, and suspends it. |
resume() |
Continues a paused sandbox from where it paused. |
fork({ name }) |
Makes a new running sandbox from this sandbox’s memory and files, and returns it. |
remove({ force }) |
Deletes the sandbox and its files. With force: true, it stops a running or paused sandbox first. |
inspect() |
Reads the sandbox’s record again. |
sandbox.info holds the record that the last call on that handle returned.
Run commands
Section titled “Run commands”sandbox.exec() runs a command and returns how it ended. A string runs under /bin/sh -c, and an array runs as it is, with no shell:
const result = await sandbox.exec("echo $GREETING from $(pwd)", { env: { GREETING: "hello" }, workdir: "/tmp" });console.log(result.stdout);The result holds exitCode, signal, stdout, stderr and lostBytes. A non-zero exit code is a result, not an error. If the command never started, for example because its binary does not exist, exec() throws CommandNotStartedError.
| Option | Value |
|---|---|
env |
Variables added to the sandbox’s environment for this command. |
workdir |
The working directory. |
user |
A user name or uid in the sandbox. |
stdin |
Text or bytes for the command to read, after which its input ends. true leaves the input open for writeStdin(). |
tty |
{ rows, cols }: runs the command on a terminal of this size, and all of its output arrives as stdout. |
signal |
An AbortSignal that ends this client’s wait. |
onStdout, onStderr |
Receive each chunk of output as it arrives. |
outputLimitBytes |
How many of the newest bytes the result keeps. |
background |
true returns a Command as soon as the command is running. |
Background commands
Section titled “Background commands”const command = await sandbox.exec("sleep 5; echo done", { background: true });console.log(command.id);const result = await command.wait();A Command has wait(), kill(), writeStdin(), closeStdin(), resize(rows, cols) and inspect(). kill() sends TERM, and kill("KILL") sends KILL. The daemon refuses any other signal.
The daemon keeps the command after this client releases it. command.disconnect() releases the stream and leaves the command running. command.reconnect() opens a new stream and replays the output that the daemon still holds. From any client, sandbox.commands.list() lists the commands the daemon holds for the sandbox, and sandbox.commands.get(id) finds one again.
Abort and output
Section titled “Abort and output”An abort never kills the remote command. It only ends this client’s wait and releases the stream, and the command keeps running until it exits or you call kill().
A result keeps the newest 8 MiB of stdout and stderr combined, so the result of a long run may hold only the end of its output. Set outputLimitBytes to keep more or less, or 0 to keep none. onStdout and onStderr still see every chunk, including the ones past the limit. result.lostBytes counts something different: the output that the daemon dropped before any client read it.
The sandbox.files methods need a running sandbox:
await sandbox.files.write("/root/notes.txt", "hello\n");console.log(await sandbox.files.read("/root/notes.txt", { encoding: "utf8" }));| Method | What it does |
|---|---|
write(path, data, { mode, parents, user }) |
Writes a string or bytes to a file. parents: true creates any missing parent directories. |
read(path) |
Returns the file’s bytes, or a string when you pass { encoding: "utf8" }. |
stat(path), list(path) |
Return the type, size, mode, owner and modification time of a path, or of each entry in a directory. |
mkdir(path), remove(path, { recursive }) |
Make a directory, or remove a path. |
upload(local, remote), download(remote, local) |
Copy one file in or out without holding it in memory. A download writes a temporary file and renames it at the end. |
uploadDir(local, remote), downloadDir(remote, local) |
Copy a whole directory in or out as a tar archive. |
sandbox.ports forwards a host port to a port on the sandbox’s 127.0.0.1:
const forward = await sandbox.ports.add(9000, 8000);console.log(forward.listening, forward.reachableOn);await sandbox.ports.remove(9000);| Method | What it does |
|---|---|
add(hostPort, guestPort, { public }) |
Forwards the host port, or changes the forward it already has. The host listens on 127.0.0.1, or on 0.0.0.0 with public: true. |
list() |
Returns the sandbox’s forwards in host port order. |
remove(hostPort) |
Ends the forward and the connections it carries. |
shard.ports() returns the forwards of every sandbox, and create() takes ports: [{ hostPort, guestPort, public }]. A Port holds listening, error and reachableOn. listening is false while the sandbox is not running or the host refuses the port, and error says why. reachableOn lists the host addresses a client connects to. A forward survives a stop and a start. See Reach a port inside a sandbox.
Processes
Section titled “Processes”sandbox.run(command, options) starts a named process in a running sandbox, under shard’s own supervisor, and returns a Process as soon as it runs. Its options are name, env, workdir, user and restart:
const ticker = await sandbox.run("for i in 1 2 3; do echo tick $i; done", { name: "ticker", restart: { policy: "no" } });console.log((await ticker.wait()).status.exit);process.stdout.write(await ticker.logs());When a process exits, its sandbox stays running, so you can still run commands in it. Use sandbox.stop() or sandbox.remove() to end the sandbox. A sandbox runs up to 16 processes. A run() of a running name throws ConflictError with the code name_taken, and a run() of an ended name replaces it.
A Process has wait(), kill(), logs(), followLogs() and inspect(). Its info holds the record that the last call on that handle returned.
nameis up to 32 lowercase letters, digits,.,_and-. Without it, the process takes the base name of its program.wait()returns theProcessInfoonce the process ends and its restart policy will not start it again.kill()ends the process withTERM, or withKILLwhen you pass{ force: true }, and cancels its restarts. The sandbox keeps running.logs()returns the output of every run of the process. The daemon keeps a bounded log, so for a long run it may hold only the end.followLogs()yields the output from the start of the log, then new output as it arrives, and ends when the process ends or the sandbox stops.restartis{ policy, retries, backoff }.policyis"no","on-failure"(after a non-zero exit code or a signal),"always"(after any exit, and whenever the sandbox starts) or"unless-stopped", the default.unless-stoppedis likealwaysuntilkill()ends the process, and a daemon restart leaves it down in a sandbox thatstop()stopped.retriescaps how many times in a rowon-failurestarts the process again, and0or no value means no cap.backoffis the first wait in seconds, 1 by default, and each later wait doubles, up to 60 seconds.
From any client, sandbox.processes.list() returns every process of the sandbox as a ProcessInfo, in the order they were first run, and sandbox.processes.get(name) finds one again. A ProcessInfo holds name, command, restart, killed, and a status with state, restarts, exit and startedAt. state is "running", "restarting", "exited", "killed", "gave-up" or "stopped".
Secrets, policies and snapshots
Section titled “Secrets, policies and snapshots”shard.secrets, shard.policies and shard.snapshots manage what lives beside the sandboxes:
const value = process.env.API_TOKEN;if (!value) throw new Error("set API_TOKEN");
await shard.secrets.set("API_TOKEN", { value, destinations: ["api.example.com"] });await shard.policies.set("api-only", [{ action: "allow", rule: "api.example.com" }]);const sandbox = await shard.create({ image: "alpine:3.20", secrets: ["API_TOKEN"], policy: "api-only", memoryMiB: 512 });The sandbox never holds the secret value. Its commands see a placeholder, and the server puts the value into the HTTPS request headers sent to api.example.com. What secrets.set() returns never holds the value either.
| Method | What it does |
|---|---|
secrets.set(name, { value, destinations, placeholder }) |
Stores a secret, or replaces its value. |
secrets.list() |
Returns { secrets, warnings }. |
secrets.remove(name, { force }) |
Deletes a secret that no sandbox holds. force: true deletes it even when a sandbox holds it. That sandbox keeps the grant, but its placeholder no longer gets a value. |
secrets.grant(sandbox, name), secrets.ungrant(sandbox, name) |
Grant a secret to a sandbox, or take it away. The sandbox must be created or stopped. |
policies.set(name, rules) |
Creates a policy, or replaces all of its rules. A sandbox that holds the policy enforces the new rules. |
policies.get(name), policies.list(), policies.remove(name) |
Read policies, or delete one that no sandbox holds. |
policies.attach(sandbox, name), policies.detach(sandbox) |
Give a sandbox a policy, or take it away. The sandbox must be created or stopped. |
snapshots.create(sandbox, { name }) |
Copies the files of a stopped sandbox into a snapshot, which outlives the sandbox. |
snapshots.list(), snapshots.inspect(ref), snapshots.remove(ref) |
Read snapshots, or delete one. |
A policy rule is { action, rule }, where action is "allow" or "deny". sandbox.egressLog() returns the network decisions the daemon holds for a sandbox, and sandbox.followEgressLog() yields the newest decisions the daemon holds, then each new one as it happens.
Capabilities
Section titled “Capabilities”Not every provider supports every verb. shard.capabilities() reports which of the nine lifecycle verbs the server supports, as true or false for each: create, start, stop, remove, pause, resume, fork, snapshot and port. swap says whether swapMiB above 0 is allowed.
const { fork } = await shard.capabilities();Calling a verb that the provider does not support throws UnsupportedError, and its message names the provider and the verb. The server never falls back to a weaker mechanism.
Errors
Section titled “Errors”Every error from the server, the connection or the settings extends ShardError:
| Error | When |
|---|---|
ConfigurationError |
A setting is missing or refused. |
ShardConnectionError |
The server is not reachable, or a stream ended before the command did. |
CommandNotStartedError |
The command never ran. |
APIError |
The server refused the request. Its subclasses are AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), InvalidRequestError (400, 413), ConflictError (409), UnsupportedError and ServerError (5xx). |
A bad local argument throws the native error instead. For example, create() with neither an image nor a snapshot throws TypeError, and upload() of a missing file throws ENOENT.