Mastra
Give a Mastra agent a Linux machine of its own. Three tools made with createTool run commands and read and write files in a froog sandbox, a full Ubuntu VM with sudo, Docker and internet.
Install
Node.js 22.13 or newer (Mastra needs it). The froog SDK is one TypeScript file that uses fetch: download it next to your code, as the SDK docs say.
$ npm init -y && npm pkg set type=module
$ npm install @mastra/core zod
$ npm install --save-dev typescript @types/node tsx
$ curl -fsSLO https://froog.tech/sdk/froog.ts # the froog client: one file, uses fetch
$ export OPENAI_API_KEY=... # your model provider's key
$ export FROOG_API_KEY=froog_... # dashboard, API keys; or: froog api-key create agents --preset ci
A complete example
Save as agent.ts next to froog.ts and run npx tsx agent.ts. The model string picks the provider; openai/... reads OPENAI_API_KEY.
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { Froog, FroogError } from "./froog";
const froog = new Froog({ apiKey: process.env.FROOG_API_KEY });
const LIMIT = 20_000; // characters of output handed back to the model
// Make sure the sandbox `name` is running: create it, or start it if it was stopped.
async function openSandbox(name: string): Promise<string> {
let state: string;
try {
state = (await froog.getSandbox(name)).state;
} catch (e) {
if (!(e instanceof FroogError) || e.status !== 404) throw e;
await froog.createSandboxes({
name,
size: "small", // 2 vCPU, 4 GB
ttl_secs: 1800, // auto-stop 30 minutes after it starts; the disk is kept
environment: false, // none of your account's variables or files
labels: { app: "mastra-demo" },
});
state = "creating";
}
if (state === "stopping") state = (await froog.waitUntil(name, "stopped")).state;
if (state === "stopped") await froog.startSandbox(name, {});
await froog.waitUntil(name, "running");
return name;
}
const clip = (s: string) => (s.length <= LIMIT ? s : "[...cut...]\n" + s.slice(-LIMIT));
// Tools bound to one froog sandbox.
function sandboxTools(sandbox: string) {
const runCommand = createTool({
id: "run-command",
description:
"Run a bash command in your Linux sandbox (Ubuntu 24.04, passwordless sudo, Docker, internet). " +
"The working directory is /home/user. Returns the exit code, stdout and stderr.",
inputSchema: z.object({
command: z.string().describe('The bash command to run, e.g. "python3 app.py"'),
timeoutSecs: z.number().int().min(1).max(3600).optional().describe("Seconds before it is killed (default 300)"),
}),
outputSchema: z.object({ exitCode: z.number(), timedOut: z.boolean(), stdout: z.string(), stderr: z.string() }),
execute: async ({ command, timeoutSecs }) => {
const r = await froog.exec(sandbox, { command, timeout_secs: timeoutSecs ?? 300 });
return { exitCode: r.exit_code, timedOut: r.timed_out ?? false, stdout: clip(r.stdout), stderr: clip(r.stderr) };
},
});
const writeFile = createTool({
id: "write-file",
description: "Create or overwrite a text file in the sandbox. Parent folders are created.",
inputSchema: z.object({
path: z.string().describe("Absolute, or relative to /home/user"),
content: z.string().describe("The whole file"),
}),
outputSchema: z.object({ path: z.string(), characters: z.number() }),
execute: async ({ path, content }) => {
await froog.writeFile(sandbox, content, { path });
return { path, characters: content.length };
},
});
const readFile = createTool({
id: "read-file",
description: "Read a text file from the sandbox.",
inputSchema: z.object({ path: z.string().describe("Absolute, or relative to /home/user") }),
outputSchema: z.object({ content: z.string() }),
execute: async ({ path }) => ({
content: clip(new TextDecoder().decode(await froog.readFile(sandbox, { path }))),
}),
});
return { runCommand, writeFile, readFile };
}
// One agent per conversation, with tools bound to that conversation's sandbox.
function codingAgent(sandbox: string) {
return new Agent({
id: "coding-agent",
name: "Coding Agent",
instructions: "You have a Linux sandbox. Use your tools to write and run code, and check results by running them.",
model: "openai/gpt-6.1-sol",
tools: sandboxTools(sandbox),
});
}
const sandbox = await openSandbox("chat-demo");
try {
const result = await codingAgent(sandbox).generate(
"Write primes.py that prints the first 20 primes, run it, and tell me the output.",
{ maxSteps: 20 },
);
console.log(result.text);
} finally {
await froog.deleteSandbox(sandbox); // or froog.stopSandbox(sandbox) to keep the disk
}
How it works
openSandboxgets the conversation's sandbox running: it creates it (asmallUbuntu VM, ready in under a minute), or starts it if it was stopped, and waits forrunning.- Three tools bound to that sandbox: run a command (
POST /v1/sandboxes/{name}/exec, up to an hour, output and exit code back), write a file and read a file (PUTandGET /v1/sandboxes/{name}/files/content). Output is cut to the last 20,000 characters so a noisy build doesn't fill the context window. - Cleanup in
finally: the example deletes the sandbox. In a real app, stop it or let its auto-stop timer stop it (see the notes). - An agent per conversation.
codingAgent(sandbox)builds the agent with tools bound to that conversation's sandbox. Agents are cheap to build.
Notes
- One sandbox per conversation or user. Name it after the conversation, thread or user id (
chat-42,user-7: 1–40 lowercase letters, digits and dashes) so every turn lands in the same machine with the same files.openSandboxabove creates it the first time and starts it again if it was stopped. Labels ("labels": {"user": "7"}) let you find a user's sandboxes later withGET /v1/sandboxes?label=user=7. - Stop instead of delete when the user may come back. The example's
ttl_secs: 1800stops the sandbox 30 minutes after it starts, busy or not (PATCHa newttl_secsto push it back while the conversation goes on). Its disk is kept for 14 days, a start takes about 15–30 seconds (about a second within 10 minutes of the stop), and installed packages and files are still there. Delete it when the conversation is over for good. - Cost. Running time is billed per second, only while the sandbox runs:
small(2 vCPU, 4 GB) is $0.0137 an hour,default(4 vCPU, 8 GB) $0.0274. A 10-minute conversation onsmallcosts about a quarter of a cent; the idle time before the TTL stops it counts as running time, so pick a TTL that matches how long users pause. A stopped sandbox costs no running time. See pricing. - Templates. Install what your agent needs once (Python packages, a repo, a database), then save it:
froog snapshot my-box --name agent-base. Create each conversation's sandbox with"from": "agent-base"and it starts with all of it, instead of the agent spending its first turns onpip install. See snapshots. - Keep your secrets out.
"environment": falsestarts the sandbox without your account's environment variables, repos and secret files, since the model runs whatever it likes in there. Pass only what that sandbox needs inenv. Use a scoped API key for your backend (froog api-key create agents --preset cican only touch the sandboxes it created) and never send it to a browser. See API keys. - More tools. The same pattern covers the rest of the API:
POST /sandboxes/{name}/urls(hostPort) gives a running web server a public URL the agent can return to the user,startJobruns servers and long builds in the background,execStreamstreams output as it arrives, andforkSandboxcopies a sandbox to try several approaches at once. Every call is in the HTTP API. - Registered agents. If you register one agent in a
Mastrainstance (formastra devor its server), the tools can't close over a sandbox name. Read the conversation's sandbox from the request context, the second argument ofexecute, instead.
Questions
Do the tools work in Mastra workflows too?
Yes. They're ordinary Mastra tools, so a workflow step can call them, or an agent inside the step can. Keep the sandbox name in the workflow's state.
Which Node version do I need?
@mastra/core asks for Node.js 22.13 or newer. The froog SDK itself runs on Node 18+, Deno, Bun or a browser.
7 days free: 25 hours of running time, 2 sandboxes at once. Then from $0.0137 an hour, billed per second.