Cogitator
Tools

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:

WorkspaceInstallCredentialsDefault root
E2BWorkspacee2bE2B_API_KEY/home/user
DaytonaWorkspace@daytona/sdkDAYTONA_API_KEY (DAYTONA_API_URL, DAYTONA_TARGET)/home/daytona
VercelSandboxWorkspace@vercel/sandboxVercel'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 with OUTSIDE_ROOT when 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 code TOO_LARGE. Nothing goes through .cogitator, which a command could swap for a symlink, and the scripts that keep snapshots in .cogitator/snapshots check 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, skipDirs listed but not entered, glob and limit applied after sorting. The sandbox sorts the entries and cuts them at limit itself, and with a glob it 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 with TOO_LARGE: narrow it with skipDirs, glob or limit.
  • remove and rename have the local rules for recursive, trash (into .cogitator/trash), overwrite, EXISTS, NOT_EMPTY and nesting. Like LocalWorkspace they are refused with BUSY while a command of the workspace runs or left processes in the background.
  • Commands run under setsid as 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 after killGrace), an abort rejects with ABORTED. background: 'kill' (default) ends what a command left running when it exits, keep leaves it until dispose(). A process that starts a session of its own (setsid) leaves the group and lives on until the sandbox goes, as with DockerWorkspace. 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 through onStdout and onStderr and capped at maxOutputBytes with truncated set. The providers' SDKs keep a command's whole output in memory (E2B's even while it streams it), so a command that prints more than maxStreamBytes (64 MiB by default) in all is stopped like on a timeout, with truncated set, exit code 143 and a note on stderr.
  • Snapshots are tar archives in .cogitator/snapshots inside the sandbox. snapshot({ label, metadata, exclude, maxFileBytes }) always leaves out .cogitator, node_modules and .git at any depth, exclude adds to them, and a file over maxFileBytes (10 MiB by default) is skipped and listed in skipped. 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 (maxFileBytes of the record) and directories that hold any of them, and reports what it restored (every file of the archive, as a tar restore rewrites them all), deleted and kept. They work with file checkpoints like a local workspace's. Names that are not UTF-8 are kept as they are. A snapshot over maxSnapshotBytes (1 GiB by default) is refused with TOO_LARGE: exclude what can be rebuilt, such as node_modules/**. Excludes of the form dir/**, dir and **/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:

OptionDefaultWhat it does
rootthe provider's directoryAbsolute, or relative to the provider's working directory
createRoottrueCreate the root when it is missing
cwd.Where commands run, relative to the root
envnoneEnvironment variables every command gets
timeout120000Milliseconds before a command is stopped
killGrace2000Milliseconds between SIGTERM and SIGKILL
maxOutputBytes1 MiBBytes of stdout and of stderr a result keeps
maxFileBytes10 MiBLargest file read and write accept
backgroundkillWhat happens to background processes by default
maxStreamBytes64 MiBOutput a command may print in all before it is stopped
maxSnapshotBytes1 GiBBytes of files a snapshot may hold

E2BWorkspace

OptionDefaultWhat it does
apiKeyE2B_API_KEYAPI key
templatebaseTemplate the sandbox is created from
sandboxIdnoneConnect to a running or paused sandbox instead
lifetime30 minutesMilliseconds of disuse before E2B kills it
metadatanoneMetadata of a created sandbox
onDisposedestroy, keep with sandboxIddestroy or keep
domainE2B's cloudDomain of a self-hosted deployment
requestTimeoutthe SDK'sMilliseconds each request may take

DaytonaWorkspace

OptionDefaultWhat it does
apiKey, apiUrl, targetDAYTONA_API_KEY, DAYTONA_API_URL, DAYTONA_TARGETCredentials and region
sandboxnoneReuse a sandbox by id or name, started when stopped
image or snapshotDaytona's defaultWhat a created sandbox runs
resourcesDaytona'scpu, memory and disk of an image sandbox
name, labels, sandboxEnvnoneName, labels and environment of a created sandbox
autoStopInterval15Idle minutes before Daytona stops the sandbox
startTimeout60Seconds to wait for the sandbox to start
onDisposedestroy, keep with sandboxdestroy, 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

OptionDefaultWhat it does
token, teamId, projectIdVERCEL_TOKEN, VERCEL_TEAM_ID, VERCEL_PROJECT_IDAccess token triple, else the OIDC token
namenoneReuse the sandbox of that name (resumed), or create it
imagevercel/sandbox/universalImage, e.g. vercel/sandbox/node:24 or your own
runtimenoneLegacy runtime such as node24, deprecated for image
lifetime30 minutesMilliseconds the sandbox runs, extended while used
vcpus, portsVercel'sSize and exposed ports of a created sandbox
onDisposedestroy, stop with namedestroy, 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.

On this page