Skip to main content

Sandboxed compute

With the computer capability the agent gets an isolated Linux sandbox: it can run commands, read and edit files, transform a spreadsheet, build and test code, then hand the result back as a download. State persists across turns in a conversation, and the sandbox pauses when idle so you don't pay for a machine nobody is using.

Enable it

  1. Capabilities: turn on Computer / sandbox for the project.
  2. Token: include computer in the capabilities you mint. Leave it out and the tools don't exist for that user.
  3. Request: enable_computer defaults to true; pass false to decline it for a turn.

That's it — there is no backend to choose, no credentials to paste and no machine spec to fill in. Every project on the platform gets the same isolated Linux machine, and Dashboard → Computer shows you what it is and what's running.

A sandbox is only provisioned if the agent actually reaches for it — enabling the capability costs nothing on turns that don't use it.

What the machine is

A microVM of its own per conversation: its own kernel, its own disk, nothing shared with another customer or another end-user. The agent is root on it, with a working sudo.

LinuxUbuntu 24.04, /workspace as the working directory
Pythonnumpy, pandas, scipy, matplotlib, pillow, openpyxl, requests, beautifulsoup4, lxml, uv
Nodenode + npm
Documentspandoc, LibreOffice, poppler, qpdf, ghostscript, tesseract + ocrmypdf, typst
Mediaffmpeg, sox, ImageMagick, libvips, the image optimisers, yt-dlp, exiftool
Developmentclang/gcc, cmake, ninja, gdb, git, sqlite3, psql/mysql/redis clients
Shell toolsripgrep, fd, fzf, jq, yq, dasel, bat, eza, tree, the archivers
Resources2 vCPU, 2 GiB RAM, 8 GiB disk

Anything missing, the agent installs itself: sudo apt-get install -y <it> works, and the change stays in that conversation's workspace.

What the agent can do

Tool
computer_bashrun a shell command (state persists between calls) — output streams to you as it appears, and it can run in the background
computer_jobscheck on or stop a command running in the background
computer_read_file / computer_write_fileread (with line offset/limit) and write text files
computer_edit_filereplace an exact string, with an ambiguity check
computer_list / computer_glob / computer_grepexplore the workspace
computer_fetch_documentcopy one of your uploaded documents in, whole — by filename or id
computer_fetch_attachmentcopy a file already in the conversation in — one it generated, one you uploaded, one it exported earlier
computer_export_filebring a sandbox file into the conversation — send_file is what hands it to the user
computer_view_filelook at an image in the sandbox to check its own work
computer_expose_portpublish a port at a URL (only where the deployment supports it)

Long commands

Two things make a slow command bearable, and both are automatic.

You see the output as it happens. While a command runs, its output arrives on the stream as command_output chunks, grouped by command_id. Append them to a pane and a five-minute test run looks like a test run instead of a hang:

await ai.chat.stream({ message: "run the test suite" }, {
onCommandOutput: ({ command_id, delta }) => appendToPane(command_id, delta),
onToken: (_d, full) => setAnswer(full),
});

Really long work runs in the background. The agent can start a build and keep working; when it finishes, the agent is told and carries on. You get a command_finished event if the turn is still going — and if it had already ended, the result arrives as a new turn in the same session, exactly like a scheduled run, so it reaches the user through whatever you already do with those (including the turn.stopped webhook if the tab is closed). Nothing to poll.

computer_view_file is the one worth knowing about: after rendering a chart the agent can open the PNG, see it, and fix it before showing the user — instead of describing a file it never looked at.

It reads before it writes

Both writers refuse a file the agent hasn't read: computer_edit_file for a file it has never seen, computer_write_file for an existing file it hasn't seen in full — that one replaces everything, including whatever it didn't look at. And both refuse again if the file changed since it was read, because a command it ran, a background job or your own user may have written to it in between; the agent reads it again and works from what is actually there.

"Hasn't read" means in the current turn. The record of what the agent has looked at is scoped to one turn, deliberately: the guarantee is not that somebody looked at the file once, it is that its current contents are in front of the model as it decides — and across turns they may have scrolled out of the window or been compacted away. So the agent re-reads before editing on a later turn. That costs one tool call and is the whole reason the guard catches a file that changed while nobody was looking.

You don't configure this and there is nothing to handle: it costs the agent a read_file call, and it is what stops a confident edit from quietly deleting a paragraph nobody knew was in the file.

A worked example — the agent pulls a document in, computes what it needs, plots it, checks the result, and hands it over:

computer_fetch_document bookings.csv # a document from the corpus, into the sandbox
computer_write_file data/revenue.csv
computer_bash python plot.py # matplotlib → out/chart.png
computer_view_file out/chart.png # look at it before claiming it's done
computer_export_file out/chart.png # into the conversation, as kind: "image"
send_file <id from the export> # ...and now the user has it

That last line is not optional. Exporting stages the file; send_file delivers it. The separation is deliberate — see Files — and leaving it out is a silent failure: the export succeeds, the file is really there, nothing errors, and attachments comes back empty.

Totals need the whole file, not search results

rag_search returns the passages that matched — a ranked fragment, never the whole document. So for anything that depends on all of a file (a total, a count, a sum, a maximum, any per-row arithmetic) the agent must computer_fetch_document it and compute in the sandbox.

It is told this, and the retrieval results say it too. Worth knowing anyway, because the failure is silent when it goes wrong: arithmetic over a fragment produces an exact-looking number that is simply short, with a row count and a filename that make it read as verified. If you have a domain where this matters, a skill naming the procedure is the strongest guarantee available.

One thing to get right for this to work: aggregate in SQL rather than pulling raw rows — only the result preview passes through the agent, so group by beats fetching 10,000 rows to sum them in Python.

Files, both directions

In. Attach a file with to_sandbox and the original bytes land in the workspace instead of being extracted to text for the model. That's what you want for data files:

await ai.chat.send({
message: "Clean this export and give me a per-region summary as CSV",
attachments: [{
kind: "file",
name: "sales.csv",
url: `data:text/csv;base64,${b64}`,
to_sandbox: true,
}],
});

The agent is told the path it landed at. Already-uploaded documents don't need re-sending — the agent copies them in itself with computer_fetch_document.

Out. Anything the agent hands over arrives on the normal channel: res.attachments, or the attachments stream event — which now fires the moment the file is produced, so a download or a screenshot appears next to the step that made it rather than after the agent has finished talking.

res.attachments;
// [{ kind: "file", url, name: "summary.csv", mime_type: "text/csv", size: 8213 },
// { kind: "image", url, name: "chart.png", mime_type: "image/png", size: 41022 }]

kind follows the file's type, so a chart the agent rendered arrives as an image your UI can show inline rather than a download card — while a CSV or a zip stays a file. URLs are signed and short-lived; download the bytes if you need them permanently.

A rendered chart also comes back into the next turn, so "same chart, but log scale" works without re-deriving it.

Sessions

Each sandbox is a session with a stable id. By default one is bound to the chat session, so a conversation keeps its workspace; pass computer_session_id to attach a specific one instead — that's how you reconnect a user to work from yesterday.

const s = await ai.computers.create(); // start one explicitly
await ai.computers.upload(s.id, file, "inbox/data.csv"); // push a file in
await ai.chat.send({ message: "Summarise inbox/data.csv", computer_session_id: s.id });
await ai.computers.pause(s.id); // stop paying while idle
await ai.computers.resume(s.id); // same workspace, later
await ai.computers.destroy(s.id);
await ai.computers.list({ include_stopped: false });
await ai.computers.exec(s.id, { command: "ls -la", timeout_s: 30 });
await ai.computers.download(s.id, "out/summary.csv"); // -> Blob

Paused sandboxes resume with the workspace intact, and you rarely need to pause one by hand: an idle sandbox does it itself, in stages.

idle forwhat happenswhat coming back costs
1 minuteit sleeps — memory written to disknothing you can feel: it wakes exactly where it was, processes and all
5 minutesmemory is dropped, files kepta boot (about a second); running processes are gone, the workspace isn't
10 minutesthe disk moves to object storagea few seconds while it comes back
7 daysit's deleteda fresh workspace

A sandbox running a command is never put to sleep, however long the command takes. And if a sandbox has genuinely gone, the next tool call recreates it rather than failing the turn.

Isolation and limits

A sandbox reaches the public internet — it can fetch a page, install from any registry, call an API you point it at. What it cannot reach is anything private: the platform's own services, its own network, another sandbox. That boundary is enforced on addresses rather than hostnames, so there is no allowlist to keep up to date.

It also cannot resolve them. The sandbox's resolver asks public DNS and never the deployment's own — otherwise a sandbox could look up the platform's internal service names and read its topology off the answers, which the address filter would block it from reaching but had already told it about. IPv6 is dropped outright, since a VM here is IPv4-only by construction and an unfiltered v6 packet is traffic no policy was written for.

The defaults:

LimitDefault
working directory/workspace — every path is confined here
CPU / RAM / disk2 vCPU, 2 GiB, 8 GiB per sandbox
per-command timeout5 minutes by default; up to your project's limit (30 min ceiling), then it's killed
command output30,000 characters, then explicitly truncated — whatever the command printed
file read/write/export20 MB
concurrent sandboxesyour project's setting; 10 by default

How long a command may run

Two of those numbers are yours, not the deployment's:

await oberik.computer.set({ execTimeoutS: 120, maxSessions: 2 });
await oberik.computer.get(); // { execTimeoutS: 120, maxSessions: 2 }

execTimeoutS is the project's own default for one command — the figure GET /computers/host reports as exec_default_timeout_s, which is why that page tells you to read it rather than hard-code five minutes. maxSessions is how many sandboxes this project may hold at once. null on either means the deployment's own.

Both are clamped at the point of use, not on the way in, and that distinction is worth knowing: computer.set({ execTimeoutS: 18000 }) stores 18000 and computer.get() reads it back, while a command on a host whose maximum is 1800 is killed at 1800 and says so ([command timed out after 1800s]). So the number you stored is what you asked for, and exec_max_timeout_s on GET /computers/host is what you will get — read both if you are sizing your own client's timeout.

Paths are validated server-side, so ../../etc/passwd is rejected before it reaches the sandbox — the model's arguments are never trusted. Isolation is a virtual machine with its own kernel, not a shared container: there's no host mount, no Docker socket, and neither your provider keys nor the JWT secret are ever placed inside it. Every command, write and export is recorded in the audit log.

One thing to keep in mind: sandbox output is untrusted input. A file the agent downloads could contain text aimed at the model. It comes back as a tool result, never as instructions, and guardrails still screen the final answer — but treat sandbox-derived content the way you'd treat user-supplied content in your own app.

Read-only sandboxes

Because the sandbox tools go through the same per-tool gate as everything else, you can hand out a look-but-don't-touch workspace with no extra machinery:

await ai.chat.send({
message: "What's in the workspace?",
allowed_tools: ["computer_list", "computer_read_file", "computer_grep", "computer_glob"],
});