FactorySandbox
A FactorySandbox is the one object a host gives MastraFactory to describe where sessions run. It owns the session sandbox constructor, the repository template built for a session, the schema of settings a user can tune per project, and optionally the ability to build that template ahead of time.
Factory stores each project's settings and passes them to create on every session boot. A setting the project leaves unset falls back to the provider's default. factory.sandboxDescription reports the provider, the settings schema as JSON Schema and the available capabilities. Everything provider-specific stays inside the object: Factory only calls the contract and reads provider for display.
UsageDirect link to Usage
Pass an instance of one of the provider classes as the sandbox option:
import { MastraFactory } from '@mastra/factory'
import { PlatformFactorySandbox } from '@mastra/platform-workspace'
export const factory = new MastraFactory({
sandbox: new PlatformFactorySandbox({
// Provider defaults for an environment that sets nothing.
defaults: { cpuCount: 4, memoryMb: 8192 },
}),
})
Provider classesDirect link to Provider classes
| Class | Package | Settings | Builds |
|---|---|---|---|
PlatformFactorySandbox | @mastra/platform-workspace | cpuCount, memoryMb, idleTimeoutMinutes | start, get |
E2BFactorySandbox | @mastra/e2b | cpuCount, memoryMb, idleTimeoutMinutes | start, get, list |
DockerFactorySandbox | @mastra/docker | baseImage, owner | start, get |
LocalFactorySandbox | @mastra/factory | None | No |
Each provider class takes the same runtime options as its sandbox class (minus id, sandboxId and template, which the class fills in per session), plus template options for the repository template and defaults for settings the environment leaves unset.
import { PlatformFactorySandbox } from '@mastra/platform-workspace'
import { E2BFactorySandbox } from '@mastra/e2b'
import { DockerFactorySandbox } from '@mastra/docker'
import { LocalFactorySandbox } from '@mastra/factory'
new PlatformFactorySandbox() // reads MASTRA_PLATFORM_ACCESS_TOKEN and MASTRA_ENVIRONMENT_ID
new E2BFactorySandbox() // reads E2B_API_KEY
new DockerFactorySandbox({ defaults: { baseImage: 'node:22-slim' } })
new LocalFactorySandbox() // reads MASTRACODE_LOCAL_SANDBOX_ROOT, defaults to ~/.mastracode/web/sandboxes
LocalFactorySandbox runs every session on the host machine under join(root, sessionId). Without env, commands only see an allow-list of host variables that git and TLS need, such as HOME, USER, SHELL and SSL_CERT_FILE, plus PATH. App secrets stay out of commands that run in repository checkouts.
Scope env to one factoryDirect link to Scope env to one factory
Every provider's env option takes a record or a function of the session's FactorySandboxContext. Use the function form when one deployment hosts several factories and a credential belongs to only one of them:
new PlatformFactorySandbox({
env: ctx => (ctx.factoryId === PLATFORM_FACTORY_ID ? { PLATFORM_TOKEN } : {}),
})
Compare factoryId, not factoryName: names can be edited and aren't unique.
Platform and Docker apply env only when they provision a sandbox. A sandbox that resumes keeps the environment it started with. If you remove a credential from some factories, recreate their running sandboxes so the credential doesn't stay in them.
Default sandboxDirect link to Default sandbox
When you omit the sandbox option and Platform credentials (MASTRA_PLATFORM_ACCESS_TOKEN or MASTRA_PLATFORM_SECRET_KEY) are set, MastraFactory installs a sandbox itself:
- Under
mastra devor withNODE_ENV=development, it installsLocalFactorySandbox, so development doesn't start remote machines. - In built deployments, it installs
PlatformFactorySandbox. This needsMASTRA_PROJECT_IDandMASTRA_ENVIRONMENT_ID. If either is missing, Factory logs a warning and turns off repository sandboxes.
An explicit sandbox option always takes precedence. Without Platform credentials and without a sandbox option, repository sandboxes are off.
For an unset setting, the provider applies its own default: 2 CPUs, 1024 MB and a 5 minute idle timeout on Platform and E2B, overridable through defaults. On Platform, a top-level idleTimeoutMinutes option also applies when defaults doesn't set one. The schema's default values show the effective defaults.
Platform polls a build through the builder that started it. After a host restart, an earlier build reads as unknown.
Keep schema changes additive: add fields, and read both names for a release when you rename one. The stored document outlives the schema that wrote it, so a key the current schema no longer declares can appear in it, for example after the host switches provider. When such a key makes an update fail validation, Factory drops the undeclared keys and retries before refusing the update. Keys the update itself sets are never dropped.
Callback formDirect link to Callback form
The bare callback sandbox: ctx => new E2BSandbox({ id: ctx.sessionId }) is also supported. Factory wraps it as a provider: 'custom' sandbox. Everything about the sandbox stays inside the callback, and the environment exposes nothing to tune.
ClassDirect link to Class
abstract class FactorySandbox<TSettings extends Record<string, unknown> = Record<string, unknown>> {
readonly provider: string
readonly settings: PublicSchema<TSettings>
create(ctx: FactorySandboxContext, settings: TSettings): MastraSandbox
template?(ctx: FactorySandboxContext, settings: TSettings): unknown
builds?: FactorySandboxBuilds<TSettings>
}
provider:
settings:
factory.sandboxDescription reports the input side as JSON Schema. Author an object of primitive fields; nested objects are not rendered. Defaults to no settings.create:
template?:
builds?:
start(ctx, settings) begins or finds a build, get(ctx, settings, buildId) reads its status and logs, and the optional list(ctx, settings) returns the history. Platform and Docker implement start and get; E2B also implements list.To write your own provider, extend FactorySandbox from @mastra/core/workspace. Factory recognizes subclasses by a brand the base class sets, so a plain object with the same shape isn't accepted.
import { FactorySandbox } from '@mastra/core/workspace'
import type { FactorySandboxContext } from '@mastra/core/workspace'
import type { PublicSchema } from '@mastra/core/schema'
interface Settings {
region?: 'us' | 'eu'
}
class MyFactorySandbox extends FactorySandbox<Settings> {
readonly provider = 'my-cloud'
readonly settings: PublicSchema<Settings> = {
type: 'object',
properties: { region: { type: 'string', enum: ['us', 'eu'], default: 'us' } },
additionalProperties: false,
}
create(ctx: FactorySandboxContext, settings: Settings) {
return new MyCloudSandbox({
id: ctx.sessionId,
sandboxId: ctx.sandboxId,
template: this.template(ctx, settings),
workingDirectory: ctx.workingDirectory,
region: settings.region ?? 'us',
})
}
// Optional: the sandbox template image Factory can build ahead of time
template(ctx: FactorySandboxContext, settings: Settings) {
return myCloudTemplate(ctx, settings)
}
// Optional: build that template ahead of time and report on it
readonly builds = {
start: async (ctx: FactorySandboxContext, settings: Settings) =>
myCloud.startBuild(this.template(ctx, settings)),
get: async (ctx: FactorySandboxContext, settings: Settings, buildId: string) =>
myCloud.buildStatus(buildId),
list: async (ctx: FactorySandboxContext, settings: Settings) =>
myCloud.builds(this.template(ctx, settings).name),
}
}
When a user recreates a stuck session's sandbox, Factory may call create with only sessionId and the persisted sandboxId (no repository access, empty settings) and then call the returned sandbox's destroy() without starting it. This happens when the server restarted or the session was opened on another replica. For recreate to work, destroy() must find and remove the session's VM from sandboxId or the session id alone. Factory doesn't offer recreate for LocalFactorySandbox, because its destroy() keeps the session directory.
FactorySandboxContextDirect link to FactorySandboxContext
Everything Factory knows about the session, passed to create, template and the build methods.
sessionId:
sandboxId?:
factoryId?:
factoryName?:
repoFullName?:
setupCommand?:
getRepositoryAccess:
repos?:
workspaceSetupCommand?:
continueOnSetupFailure?:
workingDirectory?:
resolveHead?:
Build statusDirect link to Build status
FactorySandboxBuild.status is one of pending, building, ready, failed or unknown. unknown means the provider can't read the status of a build it didn't start in this process.
Environment buildsDirect link to Environment builds
When the sandbox declares builds, factory builds the environment template ahead of sessions and registers a factory-environment-build workflow through prepare(). A build runs:
- on
POST /web/factory/projects/:id/environment/build(the Environment page's Build now, ormastra api factory project environment build <project-id>) - on a cron schedule kept in core
Schedules, enabled throughPATCH { buildTriggers: { schedule: { enabled: true, cron, timezone } } }; needs a storage adapter with the schedules domain, otherwise the route reportsscheduleAvailable: false - after a push to a linked repository's default branch when
buildTriggers.push.enabledis set, at most once perdebounceMinutes
Saving environment settings never starts a build. Scheduled and push builds skip when the last build is ready, no repository head moved and no template input changed (settings, repositories and their order, setup commands or the working directory). Factory stores only the last build id and the active template. Status and history come from the provider through GET …/environment/builds/:buildId and GET …/environment/builds (404 no_history when the provider keeps none).