Run code in a sandbox

Let your agent write and run code on a machine of its own
View as Markdown

A sandbox is a Linux machine your agent can run commands on and write files to, kept apart from everything else. Give your agent one when it needs to try code it wrote: run a script, install a package, test a fix.

By the end of this guide, your agent writes a Python script, runs it in a sandbox, and answers with the result.

Sandboxes are experimental. The API may change without a deprecation period.

Before you start

  • An agent project you can run with ast project start. Your first project sets one up.
  • The agent uses the TypeScript SDK. The sandbox client is not in the Python SDK yet.
  • You are signed in with ast login.

Give your agent a sandbox

1

Say what the sandbox needs

A new sandbox has core utilities and nothing else, so list the languages and packages your agent’s code uses. Add a sandbox section to astropods.yml:

astropods.yml
sandbox:
toolchain: auto
python:
version: "3.12"
packages: [numpy]

This sandbox gets Python 3.12 and numpy. toolchain: auto adds compilers when the sandbox installs packages, so a package that builds from source can. The sandbox section of the spec lists everything else you can declare, such as system packages and setup commands.

2

Connect the sandbox to your agent

Give each conversation its own sandbox, so one user’s files never show up in another’s conversation.

Give the agent a workspace backed by a sandbox. Mastra’s built-in workspace tools then run commands and edit files there.

agent.ts
import { Agent } from '@mastra/core/agent';
import { Workspace } from '@mastra/core/workspace';
import { AstroSandbox, serve } from '@astropods/adapter-mastra';
const agent = new Agent({
id: 'coding-agent',
name: 'Coding Agent',
instructions: 'You write Python to answer questions. Run your code before you answer.',
model: 'anthropic/claude-sonnet-4-6',
// One sandbox per conversation.
workspace: ({ requestContext }) =>
new Workspace({
sandbox: new AstroSandbox({ name: requestContext.get('threadId') }),
}),
});
serve(agent);
3

Try it locally

Start the agent:

ast project start

Open the chat at http://localhost:3100 and ask:

Use numpy to find the mean and standard deviation of 3, 8, 1, 9, 4. Run it.

The first message can take a minute while the sandbox installs Python and numpy. The agent then writes a script, runs it, and answers with the numbers. Later messages in the same conversation are quick.

Your local agent gets a real sandbox, the same kind a deployed agent gets. ast project stop deletes it.

4

Deploy

Push and deploy the agent as usual:

ast blueprint push my-agent
ast blueprint deploy my-agent

A deployed agent’s sandboxes use the sandbox section it was deployed with. After you change the section, push and deploy again.

More things your agent can do

Each recipe below is a task you can hand your agent, with what to declare and the calls a tool makes. A Mastra agent does all of these through its workspace tools, so it needs only the declaration. With any other framework, put the calls in a tool. Every snippet uses the sandboxes client and conversationId from the step above.

Analyze a spreadsheet the user uploaded

Copy the upload into the sandbox, then let Python read it.

astropods.yml
sandbox:
toolchain: auto
python:
packages: [pandas]
import { readFile } from 'node:fs/promises';
await sandboxes.writeFileBytes(conversationId, '/workspace/sales.csv', await readFile(uploadedPath));
const { stdout } = await sandboxes.exec(conversationId, {
command: ['python3', '-c', 'import pandas as pd; print(pd.read_csv("sales.csv").describe())'],
});

uploadedPath is where the user’s file landed on your agent’s disk. See Files in chat.

Read a PDF

Declare a system package, and its commands are ready to run.

astropods.yml
sandbox:
toolchain: never
packages: [poppler-utils]
await sandboxes.writeFileBytes(conversationId, '/workspace/report.pdf', await readFile(uploadedPath));
const { stdout: text } = await sandboxes.exec(conversationId, {
command: ['pdftotext', '-layout', 'report.pdf', '-'],
});

Packages come from the Amazon Linux 2023 repository. sqlite, ImageMagick and graphviz are there too. ffmpeg and pandoc are not.

Make a chart

Have the agent write a plotting script, run it, and read the image back.

astropods.yml
sandbox:
toolchain: auto
python:
packages: [matplotlib]
await sandboxes.writeFile(conversationId, '/workspace/chart.py', `
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.bar(["Mon", "Tue", "Wed"], [12, 19, 7])
plt.savefig("chart.png")
`);
await sandboxes.exec(conversationId, { command: ['python3', 'chart.py'] });
const png = await sandboxes.readFileBytes(conversationId, '/workspace/chart.png');

png is the image’s bytes. Send it to the user as a download, as Files in chat shows.

Run TypeScript

Declare Node and any command-line tool from npm.

astropods.yml
sandbox:
toolchain: never
node:
version: "22"
packages: [tsx]
await sandboxes.writeFile(conversationId, '/workspace/hello.ts', 'const n: number = 6 * 7;\nconsole.log(n);\n');
const { stdout } = await sandboxes.exec(conversationId, { command: ['tsx', 'hello.ts'] }); // "42\n"

Clone a repository and run its tests

git is always installed. An install and a test suite can outlast a single exec, so use run, which streams output and has no time limit.

astropods.yml
sandbox:
toolchain: auto
node:
version: "22"
await sandboxes.exec(conversationId, {
command: ['git', 'clone', '--depth', '1', 'https://github.com/sindresorhus/slugify', 'repo'],
});
const result = await sandboxes.run(
conversationId,
{ command: ['/bin/sh', '-c', 'npm install && npm test'], cwd: '/workspace/repo' },
{ onOutput: (chunk) => process.stdout.write(chunk.stdout) },
);

Start a long job and check on it later

spawn starts a command and returns at once, so the job keeps going after the turn ends. On a later turn, poll reports whether it finished and what it printed.

const job = await sandboxes.spawn(conversationId, { command: ['python3', 'train.py'] });
// Save job.processId with the conversation, then on a later turn:
const status = await sandboxes.poll(conversationId, job.processId);
return status.state === 'exited'
? `Done, exit code ${status.exitCode}`
: `Still running:\n${status.stdout.slice(-500)}`;

Pick up where the last message left off

Files stay in the sandbox for the whole conversation, so the agent can build on earlier work. A script it wrote three messages ago is still in /workspace, along with any data it downloaded. Ask the agent to change the script and run it again, and it edits the file in place.

What to expect

The first message in a conversation is the slow one. A new sandbox installs everything you declared before your agent’s first command runs. Sandboxes for later conversations usually start faster, because the install is reused.

Files last for the conversation, not forever. A sandbox is paused after 15 minutes without use and picks up where it left off on the next message. After an hour paused, it is removed along with its files. The next message gets a fresh sandbox with your declared software installed. Save anything that must outlast the conversation somewhere else.

A long conversation keeps its sandbox. A busy sandbox is replaced after 8 hours, and the replacement has everything the old one had: files, declared software, and anything the agent installed along the way.

Long jobs belong in the background. A single command can run for about 30 seconds. For an install, a build, or a test suite, use run, which has no time limit. With Mastra, the workspace already tells the model to run long work in the background.

A failed install shows up as an error. If a package does not exist or will not build, your agent’s first command fails with the package manager’s message. Fix the sandbox section, then deploy again, or restart ast project start when testing locally.

Troubleshooting

What you seeWhat to do
this agent declares no sandboxAdd a sandbox section to astropods.yml, then push and deploy.
the sandbox declaration failed to install at packagesA name under packages is wrong, or the package is not in the sandbox’s package repository. The same error ends in python packages or node packages for a PyPI or npm name. Check the name, then deploy again.
SandboxPreparingErrorA large declaration took longer than 15 minutes to install. Pass a larger prepareTimeoutSeconds to the client, or declare less.
An error with status 402The deployment reached its sandbox limit. Delete sandboxes you no longer need.

Next steps