Skip to content

TypeScript

Run a command in a new sandbox from Node:

Terminal window
npm install useshards
export SHARD_REMOTE=https://shard.example.com
export SHARD_API_KEY='your-api-key'
demo.mjs
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.

Terminal window
npm install useshards

The 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.

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:

Terminal window
sudo shard tokens mint --name build-agent --duration 24h

The 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.

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.

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.
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.

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.

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.

  • name is up to 32 lowercase letters, digits, ., _ and -. Without it, the process takes the base name of its program.
  • wait() returns the ProcessInfo once the process ends and its restart policy will not start it again.
  • kill() ends the process with TERM, or with KILL when 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.
  • restart is { policy, retries, backoff }. policy is "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-stopped is like always until kill() ends the process, and a daemon restart leaves it down in a sandbox that stop() stopped. retries caps how many times in a row on-failure starts the process again, and 0 or no value means no cap. backoff is 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".

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.

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.

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.