Remote Workspaces
Run an agent's workspace in a cloud sandbox on E2B, Daytona or Vercel Sandbox, with the same file tools, commands, trash and snapshots as a local workspace.
Overview
A remote workspace is a workspace whose files and commands live in a sandbox of a cloud provider instead of on your machine. The agent's tools work the same, the commands it writes run in a machine you can throw away, and nothing it does reaches your files, your network or your keys.
Three ship in @cogitator-ai/sandbox, each on its provider's SDK, an optional peer dependency you install next to it:
| Workspace | Install | Credentials | Default root |
|---|---|---|---|
E2BWorkspace | e2b | E2B_API_KEY | /home/user |
DaytonaWorkspace | @daytona/sdk | DAYTONA_API_KEY (DAYTONA_API_URL, DAYTONA_TARGET) | /home/daytona |
VercelSandboxWorkspace | @vercel/sandbox | Vercel's OIDC token, or VERCEL_TOKEN, VERCEL_TEAM_ID, VERCEL_PROJECT_ID | /vercel/sandbox |
import { Agent, Cogitator, workspaceTools } from '@cogitator-ai/core';
import { E2BWorkspace } from '@cogitator-ai/sandbox';
const workspace = new E2BWorkspace({ root: 'project', template: 'base' });
const coder = new Agent({
name: 'coder',
model: 'anthropic/claude-sonnet-5-5',
instructions: 'You work on the project in your workspace. Run the tests before you finish.',
tools: workspaceTools(),
workspace,
});
const cog = new Cogitator();
try {
await cog.run(coder, { input: 'Clone github.com/acme/parser and make its tests pass' });
} finally {
await workspace.dispose();
}Lifecycle
The sandbox is created on first use, or in start(), which also checks the sandbox has the tools the workspace needs. dispose() kills what commands left running and then releases the sandbox as onDispose says: destroy deletes it, stop stops it so its files survive (Daytona, Vercel), keep leaves it running. A sandbox the workspace created is destroyed by default, one it was pointed at (sandboxId, sandbox, name) is kept, or stopped for a named Vercel sandbox.
const workspace = new DaytonaWorkspace({ sandbox: 'agent-box', onDispose: 'stop' });Without the SDK, without credentials, or when the provider cannot create the sandbox, start() (or the first operation) fails with a WorkspaceError of code UNAVAILABLE that says which. An SDK that is installed but fails to load (a Node.js version it does not support, a broken install) is reported with its own error. e2b needs 2.28.1 or later, and its releases from 2.59 need Node.js 22.19 or later, newer than the 22.12 Cogitator itself needs.
E2B and Vercel end a sandbox after a set time. lifetime (30 minutes by default on both) sets it, and the workspace pushes the end back while it is used: before an operation once half of it has passed, and every minute while a command runs. A connected E2B sandbox gets the same lifetime. Your plan caps how long a sandbox may live. Daytona stops an idle sandbox after autoStopInterval minutes instead.
Same behavior on every provider
The drivers do only what the providers do best: create or connect to a sandbox, move bytes in and out, and run a program. Everything else runs as small shell scripts inside the sandbox through RemoteShellWorkspace, so the three behave the same:
- Paths are normalized against the root, then resolved in the sandbox with every symlink followed (
realpath -m), and refused withOUTSIDE_ROOTwhen they land outside. A symlink a command made that points out of the root is refused like any other path. - Reads and writes are bounded. A read copies at most one byte over the limit to a private file (mode 0600) of the sandbox's temporary directory before it is downloaded, so a file a command grows meanwhile never moves more than that, and reads never write the root. A write uploads to a fresh file of the temporary directory, copies it next to the file and moves it over the file in one step, keeping its mode, once the upload is checked to be the size sent.
maxFileBytes(10 MiB by default) caps both with codeTOO_LARGE. Nothing goes through.cogitator, which a command could swap for a symlink, and the scripts that keep snapshots in.cogitator/snapshotscheck it is a real directory of the root every time. What an interrupted operation leaves in the temporary directory is removed an hour later. - Listing follows the local rules: entries sorted by name with each directory's contents right after it, hidden entries left out unless
includeHidden,skipDirslisted but not entered,globandlimitapplied after sorting. The sandbox sorts the entries and cuts them atlimititself, and with aglobit sends only the entries whose name matches its last segment, so a big tree is not shipped whole. A listing over 64 MiB is refused withTOO_LARGE: narrow it withskipDirs,globorlimit. removeandrenamehave the local rules forrecursive,trash(into.cogitator/trash),overwrite,EXISTS,NOT_EMPTYand nesting. LikeLocalWorkspacethey are refused withBUSYwhile a command of the workspace runs or left processes in the background.- Commands run under
setsidas the leader of their own process group, reported with marker lines whose token arrives on stdin, so a command cannot forge them. A timeout returns exit code 124 and kills the group (SIGTERM, then SIGKILL afterkillGrace), an abort rejects withABORTED.background: 'kill'(default) ends what a command left running when it exits,keepleaves it untildispose(). A process that starts a session of its own (setsid) leaves the group and lives on until the sandbox goes, as withDockerWorkspace. Before it signals a group, the workspace checks the group still leads the session its command started, so an id the system gave to another process since is left alone. Output is streamed throughonStdoutandonStderrand capped atmaxOutputByteswithtruncatedset. The providers' SDKs keep a command's whole output in memory (E2B's even while it streams it), so a command that prints more thanmaxStreamBytes(64 MiB by default) in all is stopped like on a timeout, withtruncatedset, exit code 143 and a note on stderr. - Snapshots are tar archives in
.cogitator/snapshotsinside the sandbox.snapshot({ label, metadata, exclude, maxFileBytes })always leaves out.cogitator,node_modulesand.gitat any depth,excludeadds to them, and a file overmaxFileBytes(10 MiB by default) is skipped and listed inskipped.restore(id, { dryRun })removes what was made since, brings back what changed and leaves alone the excluded and skipped paths, files made since over the snapshot's size limit (maxFileBytesof the record) and directories that hold any of them, and reports what itrestored(every file of the archive, as a tar restore rewrites them all),deletedandkept. They work with file checkpoints like a local workspace's. Names that are not UTF-8 are kept as they are. A snapshot overmaxSnapshotBytes(1 GiB by default) is refused withTOO_LARGE: exclude what can be rebuilt, such asnode_modules/**. Excludes of the formdir/**,dirand**/name/**are left out by the sandbox itself, so their files are never listed. They live and die with the sandbox.
The sandbox is the security boundary. A command can read every file of its sandbox anyway, so the path checks keep the agent's file tools in the root rather than guard the machine. The image needs sh, GNU coreutils, findutils and tar, and util-linux setsid, which the default images of all three providers have. start() names whatever is missing.
Options
Every remote workspace takes these:
| Option | Default | What it does |
|---|---|---|
root | the provider's directory | Absolute, or relative to the provider's working directory |
createRoot | true | Create the root when it is missing |
cwd | . | Where commands run, relative to the root |
env | none | Environment variables every command gets |
timeout | 120000 | Milliseconds before a command is stopped |
killGrace | 2000 | Milliseconds between SIGTERM and SIGKILL |
maxOutputBytes | 1 MiB | Bytes of stdout and of stderr a result keeps |
maxFileBytes | 10 MiB | Largest file read and write accept |
background | kill | What happens to background processes by default |
maxStreamBytes | 64 MiB | Output a command may print in all before it is stopped |
maxSnapshotBytes | 1 GiB | Bytes of files a snapshot may hold |
E2BWorkspace
| Option | Default | What it does |
|---|---|---|
apiKey | E2B_API_KEY | API key |
template | base | Template the sandbox is created from |
sandboxId | none | Connect to a running or paused sandbox instead |
lifetime | 30 minutes | Milliseconds of disuse before E2B kills it |
metadata | none | Metadata of a created sandbox |
onDispose | destroy, keep with sandboxId | destroy or keep |
domain | E2B's cloud | Domain of a self-hosted deployment |
requestTimeout | the SDK's | Milliseconds each request may take |
DaytonaWorkspace
| Option | Default | What it does |
|---|---|---|
apiKey, apiUrl, target | DAYTONA_API_KEY, DAYTONA_API_URL, DAYTONA_TARGET | Credentials and region |
sandbox | none | Reuse a sandbox by id or name, started when stopped |
image or snapshot | Daytona's default | What a created sandbox runs |
resources | Daytona's | cpu, memory and disk of an image sandbox |
name, labels, sandboxEnv | none | Name, labels and environment of a created sandbox |
autoStopInterval | 15 | Idle minutes before Daytona stops the sandbox |
startTimeout | 60 | Seconds to wait for the sandbox to start |
onDispose | destroy, keep with sandbox | destroy, stop or keep |
Commands run in Daytona sessions, one per command (removed when it ends, kept while it has background processes to keep), and files go through the file API as bytes. Daytona records the command line of every session command, so the workspace passes env in a file of a private directory (mode 0700) of the temporary directory, which the command sources and removes before anything else, and the values never show in that record. Output of a kept command that arrives after exec returned is dropped, its log stream ends with the command.
VercelSandboxWorkspace
| Option | Default | What it does |
|---|---|---|
token, teamId, projectId | VERCEL_TOKEN, VERCEL_TEAM_ID, VERCEL_PROJECT_ID | Access token triple, else the OIDC token |
name | none | Reuse the sandbox of that name (resumed), or create it |
image | vercel/sandbox/universal | Image, e.g. vercel/sandbox/node:24 or your own |
runtime | none | Legacy runtime such as node24, deprecated for image |
lifetime | 30 minutes | Milliseconds the sandbox runs, extended while used |
vcpus, ports | Vercel's | Size and exposed ports of a created sandbox |
onDispose | destroy, stop with name | destroy, stop or keep |
Without the token triple the SDK finds the OIDC token itself: from the x-vercel-oidc-token header in a Vercel Function, or from VERCEL_OIDC_TOKEN, which vercel env pull writes to .env.local. A sandbox created without a name is not persistent, so it leaves no snapshot behind, and destroy also deletes the snapshots only that sandbox used.
Your own provider
RemoteShellWorkspace takes any RemoteShellDriver: connect to a sandbox, read and write a file by absolute path, run a program to its end, start one with streamed output and stdin, and release the sandbox. Implement those five on SSH, Modal or your own runner and the workspace brings the rest.
import { RemoteShellWorkspace, type RemoteShellDriver } from '@cogitator-ai/sandbox';
const driver: RemoteShellDriver = {
provider: 'my-runner',
defaultRoot: '/srv/work',
connect: async () => connectToMyRunner(),
};
const workspace = new RemoteShellWorkspace({ driver, root: 'project' });shellQuote and shellCommand turn an argv into a shell line for providers that only take one.
Workspace
Give an agent one directory to work in, with file tools and commands that cannot leave it, locally, in a Docker container or in a cloud sandbox, and long tool results saved there instead of lost.
WASM Tools
Run tools in isolated WebAssembly sandboxes with Extism for security, speed, and portability.