@docker/sandboxes
v0.52.0
Published
TypeScript REST SDK for Docker Cloud Sandboxes, with typed HTTP and WebSocket streams.
Keywords
Readme
Docker Sandboxes for TypeScript
Run code in isolated Docker Cloud Sandboxes from Node.js 20 or later. Your Docker account needs Cloud Sandboxes access.
npm install @docker/sandboxesSign in
The SDK gives your application a URL and code to show the user. It waits for sign-in, then keeps the session in memory and refreshes it when needed.
import { oauth, Sandboxes, SandboxesError } from '@docker/sandboxes';
const auth = oauth({
onVerification({ verificationUriComplete, verificationUri, userCode }) {
console.log(`Open ${verificationUriComplete ?? verificationUri} and enter ${userCode}`);
},
});
await auth.getAccessToken(); // Complete attended sign-in before the first API deadline starts.
const client = new Sandboxes({ auth });Attended sign-in follows the device code's lifetime, not the 30-second request timeout. Individual authentication requests remain bounded. Pass auth.getAccessToken({ signal }) to stop waiting; canceling one caller does not cancel a shared sign-in. API requests still have their own deadlines, so complete attended sign-in before making them.
OAuth credentials stay in memory by default, so a new process signs in again. For explicit local persistence, pass an intentional ignored path such as store: fileOAuthCredentialStore({path: '.docker-sandboxes/oauth.json'}); it stores plaintext credentials with restrictive POSIX permissions. Keep that path out of source control. Use a keychain, encrypted database, or secret manager for production workloads.
Custom credential stores should honor the supplied abort signal. The SDK waits for an in-flight save to settle before publishing the new credentials or starting another save.
For automation with a Docker personal access token, use the PAT authenticator. It exchanges the PAT over HTTPS for a short-lived access token, keeps it in memory, and refreshes it automatically. The API validates the token; the SDK reads its expiry only to schedule renewal.
import { pat } from '@docker/sandboxes';
declare const username: string;
declare const personalAccessToken: string;
const patAuth = pat({ username, personalAccessToken });
const automatedClient = new Sandboxes({ auth: patAuth });Custom authentication transport
If sign-in must use a proxy or custom CA configuration, pass your Fetch-compatible function to pat() or oauth(). A client's fetch option controls API requests only; it does not reconfigure a shared authenticator. To use the same transport for both:
declare const customFetch: (request: Request) => Promise<Response>;
const proxyAuth = pat({
username,
personalAccessToken,
fetch: customFetch,
transportRetries: 'none',
});
const proxyClient = new Sandboxes({
auth: proxyAuth,
fetch: customFetch,
transportRetries: 'none',
});Each authenticator retains its custom function for initial sign-in and refresh. oauth() uses it for device-code requests and token polling too. The function must support concurrent requests, honor the request's abort signal and redirect policy, and make one attempt per call. Declare transportRetries: 'none' on each owner using custom fetch. Authentication keeps its existing deadlines and does not retry or fall back to global fetch when the custom transport fails. Without custom fetch, authentication uses the platform's global fetch. Closing a client does not close its authenticator or either borrowed transport.
By default, the same access token, token provider, or sign-in session authenticates Sandbox requests at https://connect.docker.com/sandboxes and personal Governance requests at https://hub.docker.com/rpc. Sign-in does not read or change Docker CLI credentials.
Inherited environment variables can change where the SDK sends credentials. Unless you intentionally use internal integration overrides, unset SANDBOXES_API_URL, DOCKER_OAUTH_ISSUER, DOCKER_TOKEN_EXCHANGE_URL, and DOCKER_GOVERNANCE_URL before creating clients or authenticators.
These controls are retained for internal testing, not supported end-user configuration.
| Variable | Selects | Default |
| --- | --- | --- |
| SANDBOXES_API_URL | Management API base, without /v1 | https://connect.docker.com/sandboxes |
| DOCKER_OAUTH_ISSUER | OAuth issuer and its device and token endpoints | https://login.docker.com/ |
| DOCKER_TOKEN_EXCHANGE_URL | PAT exchange endpoint | https://hub.docker.com/v2/auth/token |
| DOCKER_GOVERNANCE_URL | Personal Governance RPC base | https://hub.docker.com/rpc |
Each omitted variable keeps its own production default. Governance is not inferred from the management URL. Overrides must be nonempty HTTPS URLs without user information, a query or a fragment. A management base ending in a version such as /v1 is rejected rather than rewritten.
Set overrides before constructing pat(), oauth() or Sandboxes. Each retains its initial configuration; later environment changes do not redirect requests or token refreshes. A client using a Docker authenticator must match that authenticator's management and Governance bases. An explicit baseUrl takes precedence over SANDBOXES_API_URL, but does not bypass override validation or this authority check. Use a separate OAuth credential store for each issuer; stored records are not bound to an issuer.
The recommended credential configuration is auth: use oauth() for attended sign-in, pat() for automation, bearer() for an existing access token, or a custom Authenticator when the application owns token acquisition. SandboxesOptions also supports accessToken for a fixed bearer token and tokenProvider for a token read on each request.
Create a sandbox and run code
Creation returns when the server accepts the request. Wait separately when your next step needs a running sandbox. Later examples reuse client and sandbox.
const accepted = await client.create({
imageRef: 'python:3.12',
displayName: 'python-worker',
resources: { cpus: 2, memoryMib: '4096' },
});
const sandbox = await accepted.waitUntilRunning();
const result = await sandbox.processes.run({
args: ['python', '-c', 'print("Hello from a sandbox")'],
});
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
console.log(result.exitCode);
await sandbox.files.write('/tmp/message.txt', 'Hello\n');
const message = await sandbox.files.read('/tmp/message.txt', { encoding: 'utf8' });
console.log(message);Waiters default to five minutes. Pass { timeoutMs: 60_000 } to change the deadline. A timeout leaves the sandbox in place.
Use a returned name to reconnect. List one page or iterate over all pages:
const existing = await client.get(sandbox.name);
for await (const item of client.allSandboxes({ query: { pageSize: 25 } })) {
console.log(item.name, item.core.status);
}Pagination has no default total timeout; each page request retains its 30-second timeout. An explicit timeoutMs bounds the whole traversal from the start of iteration, including time spent processing items. Caller cancellation, client closure and an enclosing workflow deadline stop traversal before another request or buffered item. collect() and listAll*() retain all results in memory.
Launch a bundled kit
A kit supplies a sandbox image and its agent configuration. The SDK includes a versioned catalog; list() reads it locally. The server must support kit launches, and the selected agent may need its own credentials.
console.log(client.kits.list());
const agentSandbox = await client.kits.launchAndWait('claude', {
displayName: 'coding-agent',
resources: 'small',
});launchAndWait() creates the sandbox and waits for its running state under one deadline. Use launch() when you want the accepted handle immediately and will wait separately. Neither starts an agent task or checks files created by kit setup.
Kit launches default to Small (2 vCPUs, 4 GiB). Choose micro, small, medium, large, or xl through resources, or supply an explicit CPU/memory object. The same named sizes work with client.create().
Manage personal network policies
Governance uses the same client credentials. These methods manage policies owned by the signed-in user; they do not select an organization or another user. This example creates an empty allowlist. Add the required network rules before attaching it to a sandbox.
const policy = await client.governance.policies.create({
name: 'project-network',
policyType: 'POLICY_TYPE_ALLOWLIST',
allowlist: { rules: [] },
});
const policies = await client.governance.policies.list();
const currentPolicy = await client.governance.policies.get(policy.id);
const updatedPolicy = await client.governance.policies.update(
policy.id,
{ description: 'Network access for this project' },
{ updateMask: 'description' },
);
await client.governance.policies.delete(policy.id);To use an existing policy when creating a sandbox, pass its ID in network.policyIds.
Handle errors and retries
HTTP failures throw SandboxesError. Wait failures retain the last observed resource in WaitError.resource.
try {
await client.get(sandbox.name, { timeoutMs: 30_000, maxRetries: 0 });
} catch (error) {
if (!(error instanceof SandboxesError)) throw error;
console.error(error.message, error.requestId);
}The default is at most two additional attempts for transient failures on safe unary requests. Supported mutations get one idempotency key per call, reused with the same body on retries. Supply { idempotencyKey: 'your-request-id' } to reuse a key across application calls. maxRetries: 0 disables retries for a call or client. processes.run(), raw streams and Governance mutations are never replayed.
processes.run() has no default total deadline; process creation and each output read retain their 30-second request timeout. Pass { timeoutMs: 300_000 } as its second argument to bound the whole run, including creation. Caller cancellation, client closure and an enclosing workflow deadline also stop local waiting. run() does not kill the process; withSandbox() retains its separate sandbox cleanup policy. After creation, a failure retains the process identity in ResourceWaitError.resource.
process.connect() automatically reconnects while you consume output after a temporary disconnect. It resumes after the last chunk delivered to your iterator, without restarting the process or replaying input. Each recovery episode has a 30-second budget. Set { reconnect: false } or { reconnectTimeoutMs: 60_000 } in the second argument to control recovery. Authentication failures, service refusals and invalid output cursors stop the connection.
Connection setup has a 30-second timeout, but an attached process session has no default lifetime limit. An explicit timeoutMs, abort signal or enclosing workflow deadline still applies to the whole session. A failed input write may have reached the process; inspect its state before sending the input again.
Custom fetch implementations and borrowed clients require transportRetries: 'none': disable any retries inside that transport first. Each call retains its selected credential; the SDK never refreshes it to rescue an in-flight call. After a token rejection, a later independent call can renew the managed login or PAT. Retries and waiters use valid server delay guidance exactly when the deadline allows it.
Clean up
Fetch the latest resource before deleting it. HTTP 204 confirms deletion; otherwise, wait on the returned resource. An unexplained 404 does not confirm deletion.
const latest = await sandbox.refresh();
const deleting = await latest.delete();
if (deleting) await deleting.waitUntilDeleted();
const latestAgent = await agentSandbox.refresh();
const deletingAgent = await latestAgent.delete();
if (deletingAgent) await deletingAgent.waitUntilDeleted();
await automatedClient.close();
await client.close();close() cancels client-owned work and clears its in-memory credentials. It does not delete sandboxes or revoke remote credentials.
Advanced HTTP and browser use
client.api exposes explicit request envelopes. For a conditional read, pass If-None-Match; a 304 response becomes undefined. client.api.raw retains HTTP status and response headers.
Browser bundles support sandbox management, process WebSockets and raw file reads where the service permits cross-origin requests. Streaming uploads and multipart transfers require Node.js. Closing a process connection detaches it without killing its process.
See the API specification for request fields and supported operations.
