plimsoll home · All integrations

plimsoll in a Mastra agent

plimsollExecuteCode gives a Mastra agent an executeCode tool with one sandbox per user and thread. Unlike the fresh-run pages (Agno, CrewAI, Google ADK), variables, loaded data and files survive between the calls and turns of that conversation, until the application calls dispose, 10 minutes pass without a call, or the same user opens a fourth conversation and this one is that user's least recently used. The key needs both IDs Mastra hands the tool, resourceId and threadId; without either, every call runs fresh. The price: each conversation's sandbox is one trust domain, and an idle one holds a daemon slot until it closes.

Mastra: an open-source TypeScript framework for building AI agents; an agent's memory groups messages into threads, and each thread belongs to a resource, usually a user. Mastra (EXTERNAL · official docs ↗)

The Professor (a fictional narrator)

Every user's thread gets its own sandbox. Two people may both call their conversation lesson; they still do not share a desk.

Wire it up

npm install @plimsollmark/client @mastra/core zod
import { Agent } from "@mastra/core/agent";
import { PlimsollClient } from "@plimsollmark/client";
import { plimsollExecuteCode } from "@plimsollmark/client/mastra";

const { executeCode, dispose } = plimsollExecuteCode({
  client: new PlimsollClient({ baseUrl: process.env.PLIMSOLL_URL!, token: process.env.PLIMSOLL_TOKEN }),
  minimumIsolation: "kernel", // the default, written out; "container" for local docker
});
export const analyst = new Agent({ id: "analyst", name: "analyst", instructions: "...", model, tools: { executeCode } });

// The IDs come from your authenticated application, never from the model.
// Mastra hands them to the tool as resourceId and threadId.
await analyst.generate(prompt, { memory: { resource: userId, thread: conversationId } });

// When the conversation ends:
await dispose(conversationId, userId);

Options, limits and errors: TypeScript client guide, Mastra (EXTERNAL · source repo ↗)

Follow one call

Replay a recorded scenario. The diagram lights each hop the call passes; a refused call stops where it is refused. These buttons send no execution requests. Each answer's source states how it was recorded, including any fixture used. Exception panels show what reached the application, rather than an invented code result.

The modelasks for executeCodeSTOPPED HEREMAY HAVE RUNThe language model writes the arguments: the code, its language and any text files. It never sees or chooses the user, the thread, the sandbox or the floor.Mastra agentmemory: user, threadSTOPPED HEREMAY HAVE RUNA Mastra Agent runs the model's tool calls. The application passes memory: { resource, thread } to generate or stream, and Mastra hands them to the tool as resourceId and threadId.executeCodecreateToolSTOPPED HEREMAY HAVE RUNThe Mastra tool from plimsollExecuteCode. It builds its key as { owner: resourceId, conversation: threadId }, which CodeSandboxes encodes as the JSON pair [resourceId, threadId]; with either missing there is no key, and the call runs fresh.CodeSandboxesper user and threadSTOPPED HEREMAY HAVE RUNHolds one plimsoll session per key in this process's memory and sends each call to it as a cell. dispose(threadId, resourceId) closes it, and so do 10 minutes without a call (idleCloseMs); a user's fourth thread first closes that user's least recently used sandbox with no call in flight (maxSessionsPerOwner). A call without a key gets its own sandbox, deleted afterwards. The client checks every answer's run record and isolation tier.plimsolldsession + floorSTOPPED HEREMAY HAVE RUNOpens the session only behind the floor the tool asked for, or refuses before anything runs; runs each call and states a run record for it.The sandboxkept per thread, or freshSTOPPED HEREMAY HAVE RUNA locked-down container with no network. For a keyed call it is the thread's session sandbox, where a Python and a JavaScript interpreter stay alive between calls and files stay in the working directory; for a call without a key, a new one that is deleted after the call.The modelasks for executeCodeSTOP?The language model writes the arguments: the code, its language and any text files. It never sees or chooses the user, the thread, the sandbox or the floor.Mastra agentmemory: user, threadSTOP?A Mastra Agent runs the model's tool calls. The application passes memory: { resource, thread } to generate or stream, and Mastra hands them to the tool as resourceId and threadId.executeCodecreateToolSTOP?The Mastra tool from plimsollExecuteCode. It builds its key as { owner: resourceId, conversation: threadId }, which CodeSandboxes encodes as the JSON pair [resourceId, threadId]; with either missing there is no key, and the call runs fresh.CodeSandboxesper user and threadSTOP?Holds one plimsoll session per key in this process's memory and sends each call to it as a cell. dispose(threadId, resourceId) closes it, and so do 10 minutes without a call (idleCloseMs); a user's fourth thread first closes that user's least recently used sandbox with no call in flight (maxSessionsPerOwner). A call without a key gets its own sandbox, deleted afterwards. The client checks every answer's run record and isolation tier.plimsolldsession + floorSTOP?Opens the session only behind the floor the tool asked for, or refuses before anything runs; runs each call and states a run record for it.The sandboxkept per thread, or freshSTOP?A locked-down container with no network. For a keyed call it is the thread's session sandbox, where a Python and a JavaScript interpreter stay alive between calls and files stay in the working directory; for a call without a key, a new one that is deleted after the call.

The model asks for

The tool's answer, or the error it threw

The Professor (a fictional narrator)

Each step of the call

  1. The model: The language model writes the arguments: the code, its language and any text files. It never sees or chooses the user, the thread, the sandbox or the floor.
  2. Mastra agent: A Mastra Agent runs the model's tool calls. The application passes memory: { resource, thread } to generate or stream, and Mastra hands them to the tool as resourceId and threadId.
  3. executeCode: The Mastra tool from plimsollExecuteCode. It builds its key as { owner: resourceId, conversation: threadId }, which CodeSandboxes encodes as the JSON pair [resourceId, threadId]; with either missing there is no key, and the call runs fresh.
  4. CodeSandboxes: Holds one plimsoll session per key in this process's memory and sends each call to it as a cell. dispose(threadId, resourceId) closes it, and so do 10 minutes without a call (idleCloseMs); a user's fourth thread first closes that user's least recently used sandbox with no call in flight (maxSessionsPerOwner). A call without a key gets its own sandbox, deleted afterwards. The client checks every answer's run record and isolation tier.
  5. plimsolld: Opens the session only behind the floor the tool asked for, or refuses before anything runs; runs each call and states a run record for it.
  6. The sandbox: A locked-down container with no network. For a keyed call it is the thread's session sandbox, where a Python and a JavaScript interpreter stay alive between calls and files stay in the working directory; for a call without a key, a new one that is deleted after the call.

How thick should the walls be?

The executor carries a floor; the daemon states its isolation tier. This picker is a teaching simulation: change either to see whether the tier meets the floor. It sends no request and does not check other capabilities.

The Professor (a fictional narrator)

plimsollExecuteCode's floor defaults to kernel (gVisor): docker under runc is the container tier and refuses every call until it runs gVisor, or the tool is built with minimumIsolation container, which is for development on your own code only. Keep kernel, or set vm, for code you did not write. With vm, e2b keeps sessions (a suspend ends the interpreter) and dockercloud keeps none, so there every call runs fresh.

Surprises and limits

One sandbox per user and threadEvery call in one user's thread shares a sandbox: variables, imports and loaded data survive between calls and turns, and files stay in the working directory. That makes the conversation one trust domain: code from an early call can leave variables, files or patched functions behind that later calls run with. The fresh-run integrations give every call a new sandbox instead.
The key needs both IDsThe add-on keys the sandbox by { owner: resourceId, conversation: threadId }, which Mastra takes from the memory option your application passes to generate or stream. Without either one, every call runs fresh: under an empty resource, two users whose thread IDs collided would share a sandbox. Set the resource from your authenticated user, never from anything the user typed or the model wrote.
Close it yourselfMastra has no hook for the end of a conversation. Call dispose(threadId, resourceId) when it ends; otherwise the sandbox closes after idleCloseMs (10 minutes by default) without a call, and holds a daemon slot until then. sandboxes.disposeAll() closes every sandbox the process holds, for shutdown. One user holds at most maxSessionsPerOwner sandboxes (3 by default: a person rarely works in more than a few conversations at once): a fourth thread first closes that user's least recently used sandbox with no call in flight, and that thread's next call says freshSandbox, its variables and files gone, never an answer. In an agent network Mastra fills a missing resourceId with the network's name, so pass owner, the user from your own authentication, or every user of the network counts as one owner and they close each other's sandboxes.
The model sees only the messageWhen the tool throws (a refusal, a lost answer), Mastra hands the model the error's message as error-text, as the floor scenario's second panel shows: the notDispatched mark does not reach it. Only that mark says nothing ran; without it a call may have run, so do not let anything replay it automatically.
Kernel floor unless you lower itplimsollExecuteCode requires the kernel tier (docker under gVisor, as the daemon's startup checks verified) unless you pass minimumIsolation, so an ordinary docker daemon under runc, the container tier, refuses every call before anything runs. Pass container for local development on your own code; code you did not write needs kernel or vm, and with vm, e2b keeps sessions (a suspend ends the interpreter) while dockercloud keeps none, so there every call runs fresh.
One process, one set of sandboxesThe sandboxes live in the memory of the process that built the tool. A conversation that continues in another process (a second server, a restart) gets a new sandbox, and its first call says freshSandbox. The per-user cap counts this process's sandboxes only; across processes, set the daemon's SANDBOX_MAX_SESSIONS_PER_OWNER: every open names its resourceId to the daemon as a digest keyed by the caller token, and the daemon holds each user to the cap across all of them. The resourceId must be the user your server authenticated: a user who could choose it could close another user's sandboxes.
These buttons replay evidenceMost scenarios called the real tool's execute with the agent context Mastra passes it; two went through a real Mastra Agent with a scripted model. All ran against a local daemon on docker under runc, and no model service was called. Scripted answers test the plumbing, not a model's judgment.