Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/services/plugins/builtin-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ export const BUILTIN_PLUGINS = [
}));

export const OFFICIAL_PLUGINS = [
{
name: "environment-modal-sandbox",
pluginId: "environment-modal-sandbox",
defaultEnabled: true,
},
{
name: "browser-automation",
pluginId: "browser-automation",
Expand Down
1 change: 1 addition & 0 deletions apps/server/test/services/plugins/builtin-plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ describe("builtin plugin reconciliation", () => {
it("keeps official plugins bundled but out of the auto-install builtins", () => {
const optionalNames = OFFICIAL_PLUGINS.map((plugin) => plugin.name);
expect(optionalNames).toEqual([
"environment-modal-sandbox",
"browser-automation",
"github",
"docs",
Expand Down
4 changes: 4 additions & 0 deletions docs/cli-guide-and-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ personal-workspace paths skip both hooks. These semantics apply equally to CLI,
SDK, and app launches; see [worktrees.md](worktrees.md).

The Machines settings creation drawer prepares an existing-machine command when access is ready, otherwise shows setup guidance. After access is ready, Choose a machine provider lets users select a project before reviewing provider inputs; the selected project is passed to the same `hosts.submit`/`bb machine create --project` launch surface.

Modal connection and machine commands are documented in [modal-sandboxes](../plugins/environment-modal-sandbox/skills/modal-sandboxes/SKILL.md). `bb modal image show [--json]` reads the Dockerfile shown in settings; `bb modal image set --file PATH [--json]` saves a validated plugin-wide override and `bb modal image reset [--json]` restores the bundled default for future machines; `bb modal account inspect --json` checks credentials; `bb machine create --provider modal-sandbox --project PROJECT --json` automatically prepares the bundled image and installs the daemon. `bb machine remove MACHINE --yes` explicitly removes compute and private snapshots.

Modal image debugging uses `bb modal image build`, `bb modal sandbox run`, `bb modal sandbox exec ID [--json] -- COMMAND...`, and `bb modal sandbox stop ID`. Debug compute expires after 30 minutes and skips BB enrollment and project setup. See the plugin skill for output limits and typed RPC equivalents.
31 changes: 31 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1279,3 +1279,34 @@ server gh credentials to machines; `true` enables them again. In Machines →
Advanced settings, the automatic GH_TOKEN switch controls the same setting.
This does not log the server out or suppress an explicit custom GH_TOKEN.
Changes apply to new turns, setup commands and terminals.

## Modal machines

The optional Modal sandbox plugin builds/reuses a standard tools image for new
machines. Settings can save a shared Dockerfile override or reset to the bundled
default. CLI: `bb modal image show`, `bb modal image set --file PATH`, and
`bb modal image reset` (append `--json`). Typed plugin RPCs `image.definition`,
`image.set({dockerfile})`, and `image.reset` expose the same persistent definition.
Supported instructions are one FROM followed by RUN, ENV, WORKDIR, and USER; no
build context or multi-stage builds. Saving does not build or modify existing
machines. The next new machine uses the saved definition. BB installs the daemon
on demand, then clones the project and runs its setup hook.

Configure `tokenId` and `tokenSecret` in secret plugin settings; `appName` defaults
to `bb-sandboxes`. `cpu` and `memoryMiB` default to blank (Modal defaults).
`idleMinutes` defaults to 15 (0 disables idle suspension), and `timeoutMinutes`
defaults to 1440 with an allowed range of 1–1440. Existing machines use current idle
policy; running compute keeps its vendor deadline and restored compute uses the
current lifetime. Resource reservations stay pinned across restore.

There is no automatic retention removal. Use `bb machine remove MACHINE --yes`
for explicit cleanup. Manual and idle pauses save a filesystem snapshot before
terminating compute. There is no pre-expiry scheduler: a sandbox that stays active
until its configured timeout can lose changes since its last successful pause.
Provider details expose expiry and saved-image status; missing compute never
silently restores stale state. Open terminals prevent idle suspension.

`bb modal account inspect --json` tests credentials without allocating resources.
Create with `bb machine create --provider modal-sandbox --project PROJECT --json`.
See [modal-sandboxes](../plugins/environment-modal-sandbox/skills/modal-sandboxes/SKILL.md)
for prerequisites and lifecycle commands.
15 changes: 15 additions & 0 deletions packages/plugin-build/src/builtin-server-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,18 @@ describe("builtin server artifacts", () => {
{ pluginDir: "environment-project-checkout" },
{ pluginDir: "environment-git-worktree" },
{ pluginDir: "environment-personal-workspace" },
{ pluginDir: "environment-modal-sandbox" },
])(
"inlines the environment-provider runtime into the $pluginDir server entry",
async ({ pluginDir }) => {
const root = await mkdtemp(join(repositoryRoot, ".builtin-server-test-"));
tempDirs.push(root);
const source = join(repositoryRoot, "plugins", pluginDir);

const fileNames = (await readdir(source)).filter(
(fileName) =>
fileName === "package.json" ||
fileName === "Dockerfile" ||
fileName.endsWith(".ts") ||
fileName.endsWith(".svg"),
);
Expand All @@ -98,6 +101,18 @@ describe("builtin server artifacts", () => {
);
const built = await buildPluginServer(root, "0.9.0-test", toolchain);

if (pluginDir === "environment-modal-sandbox") {
await cp(join(source, "scripts"), join(root, "scripts"), {
recursive: true,
});
await import(
pathToFileURL(join(root, "scripts/stage-assets.mjs")).href
);
expect(await readFile(join(root, "dist/Dockerfile"), "utf8")).toEqual(
await readFile(join(source, "Dockerfile"), "utf8"),
);
}

const bundle = await readFile(built.jsPath, "utf8");
expect(
bundledSdkSpecifiers(bundle).filter(
Expand Down
13 changes: 8 additions & 5 deletions packages/templates/src/templates/bb-guide-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -901,14 +901,17 @@ composer regions). Thread Hover
Cards installs from the BB Community marketplace (source: the bb-plugins
repo).

Modal image setup uses `bb modal project inspect`, `recipe put/show/list`,
`context upload`, `image build/logs/status/cancel/list/gc`, and
`project configure/show`. Every command supports `--json`. Read the Modal
plugin's command reference for required arguments, reviewed dirty contexts,
revision CAS, explicit rebuilds, and garbage collection grace periods.
Modal setup uses `bb modal account inspect --json` to check credentials, then
`bb machine create --provider modal-sandbox --project PROJECT --json` to create a
machine. Settings edits its shared Dockerfile; `bb modal image set --file PATH [--json]` saves it and `bb modal image reset [--json]` restores the bundled default for future machines; `bb modal image show [--json]`
reads the same file without cloud access. The image builds automatically and is reused across projects;
core installs the daemon on demand. Project dependencies and services belong in
`.bb-env-setup.sh`. Read the plugin's skill for connection and lifecycle details.

Contributed commands may accept `--stdin`: the calling CLI transfers up to
256 KiB of multiline text as `--input-text`, without reading server-local files.
The existing `--<flag>-stdin` form still accepts one line. A plugin can follow
long-running jobs with bounded `experimental_continue` response pages; stopping
the CLI stops the reader, not the job.

Modal image debugging: `bb modal image build [--json]` prepares the saved image; `bb modal sandbox run [--json]` starts a 30-minute standalone sandbox; `bb modal sandbox exec ID [--json] -- COMMAND...` runs a command (60-second timeout); `bb modal sandbox stop ID [--json]` cleans up. These debug sandboxes skip BB enrollment, clone and setup. Logs are returned after the build finishes.
4 changes: 4 additions & 0 deletions plugins/bb-official.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,9 @@
"machine-manual": {
"category": "environments",
"screenshots": []
},
"environment-modal-sandbox": {
"category": "environments",
"screenshots": []
}
}
29 changes: 29 additions & 0 deletions plugins/environment-modal-sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Debian with Node.js and npm, pinned for reproducible machine images.
FROM node:22.19.0-bookworm-slim@sha256:4a4884e8a44826194dff92ba316264f392056cbe243dcc9fd3551e71cea02b90

# Git/GitHub access, HTTP and JSON tools, fast search, and native build support.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
gh \
jq \
ripgrep \
procps \
build-essential \
python3 \
&& rm -rf /var/lib/apt/lists/*

# Package manager and coding agents. Versions match the supported BB providers.
RUN npm install -g \
pnpm@9.15.0 \
@earendil-works/pi-coding-agent@0.84.0 \
@openai/codex@0.153.4 \
@anthropic-ai/claude-code@2.1.263 \
&& npm cache clean --force

# Run agents as an unprivileged user with a writable home directory.
# BB installs its daemon on demand during bootstrap; it is not baked into this image.
USER node
WORKDIR /home/node
167 changes: 167 additions & 0 deletions plugins/environment-modal-sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Modal sandbox

Run reusable BB machines in Modal. Install the optional official plugin, connect
its token in Settings → Plugins → Modal sandbox, select a project, and create a
machine. The project picker exposes **New sandbox** under **New machine**.

## Standard image

Settings lets you edit, save, or reset the Dockerfile used for future machines
across all projects. Agents can use the same saved definition through the CLI:

```sh
bb modal image show > Dockerfile
bb modal image set --file ./Dockerfile
bb modal image show --json
bb modal image reset
```

Only one `FROM` followed by `RUN`, `ENV`, `WORKDIR`, or `USER` is supported.
Comments and line breaks are preserved. There is no build context, `COPY`, `ADD`,
or multi-stage build. Invalid definitions leave the saved version unchanged.
Saving/resetting does not build or allocate compute; existing machines and their
snapshots are unaffected. The last saved definition applies to new launches.
The override is stored by this plugin and survives reloads; reset uses the bundled
Dockerfile from the installed plugin version.

`--file` resolves relative to the invoking CLI directory. In a BB thread it reads
from that thread's host; without thread context it reads on the server's primary
host. Remote callers without thread context can use the typed RPC with file text.
All commands accept `--json` as the final flag. `image.definition`, `image.set`
(input `{dockerfile}`), and `image.reset` are available through `modalRpcContract`
and `sdk.plugins.callRpc`. They return `{dockerfile, customized}`. Definitions
are limited to 65,536 characters. Editing needs no Modal credentials.

The plugin ships a [Dockerfile](Dockerfile) with Debian, Node, Git/GitHub CLI,
build tools, Python, ripgrep, jq, pnpm, Pi, Codex and Claude Code. It contains no BB
daemon, project files, enrollment state or credentials. The image is named by the
Dockerfile's SHA-256 and reused within the Modal account. The first launch builds
and publishes it automatically; later launches reuse it. Changing the Dockerfile
creates a new image version for future machines. Modal also caches build layers.
There is no project recipe, uploaded context, smoke-test gate or image promotion.

Machine creation reports image preparation and allocation progress. Build failures
surface on the machine launch and may be retried. Cancelling a launch prevents
subsequent sandbox allocation; an image build already submitted to Modal can
finish and remain cached. Shared standard images are not removed with a machine.

Core prepares enrollment before allocation. As soon as the sandbox ID is known,
the plugin awaits a durable resource checkpoint before bootstrap, so cancellation
cleanup does not need to allocate or enroll again. Core installs the matching BB
daemon on demand using Modal exec, enrolls the machine and waits for its connection.
Restore uses the same bootstrap API, reusing an enrolled daemon from the snapshot when available.
Bootstrap credentials travel through stdin and are never persisted in machine
resources. Core owns machine access grants and runtime credential injection.

Core clones the selected project and runs its `.bb-env-setup.sh`. Use that hook to
install project dependencies and start services; failures remain visible in the
launch logs. The hook must be idempotent because it runs again after filesystem
restore. Environment teardown uses the core `.bb-env-teardown.sh` hook. Attached,
user-maintained checkouts skip owned-environment hooks. `.worktreeinclude` does not
apply to fresh clones; configure runtime files and secrets through core Machine
environment settings.

## Settings and commands

| Setting | Meaning |
| ------------------------ | --------------------------------------------------------- |
| `tokenId`, `tokenSecret` | Required Modal token, entered in secret settings. |
| `appName` | Modal app, default `bb-sandboxes`. |
| `timeoutMinutes` | Compute lifetime, 1–1440 minutes; default 1440. |
| `idleMinutes` | Pause after idle, default 15; 0 disables idle suspension. |
| `cpu`, `memoryMiB` | Resource reservations; blank uses Modal defaults. |

Use `bb modal account inspect --json` to validate credentials
without allocating resources. Create with
`bb machine create --provider modal-sandbox --project PROJECT --json`, or SDK
`hosts.submit({machineProviderId:"modal-sandbox",projectId,key})`. Machine creation
accepts no per-machine image inputs; configure the shared Dockerfile separately. Account inspection is also available through the
plugin's typed `modalRpcContract` (`account.inspect`) and `sdk.plugins.callRpc`.
See the [command reference](skills/modal-sandboxes/SKILL.md).

## Lifecycle

Defaults are a 15-minute idle pause and 24-hour compute lifetime. Open terminals
prevent idle pause. The current idle setting applies to existing machines; new
compute uses the current lifetime. There is no automatic retention removal.

Manual and idle pauses snapshot the filesystem before terminating compute. Core
blocks new work, interrupts turns and closes terminals; the plugin stops the
daemon, saves the filesystem and durably records the snapshot before termination.
Resume preserves host identity and reruns setup. Interrupted turns are not replayed.

There is no pre-expiry scheduler. A sandbox that stays active until its configured
timeout (24 hours by default) stops without a guaranteed final snapshot. Changes
since the last successful pause may be lost. Pause before the timeout to save work.
Failed saves retain compute while it still exists.
Machine provider details expose vendor state, expiry and the saved image. Missing
compute never silently becomes an empty checkout or an older snapshot. A checkpoint
from an interrupted planned suspension remains recoverable.

Use `bb machine lifecycle MACHINE --json` for core state and `bb machine show
MACHINE --json` for machine details. `sdk.hosts.experimental_providerDetails`
returns Modal status. Remove explicitly with `bb machine remove MACHINE --yes`.
Account identity remains pinned; restore the original account before lifecycle
operations. Removal deletes private snapshots and compute, retaining the shared
standard image. Removing lost compute can remain blocked on core checkout cleanup.

## Prerequisites

Modal credentials, a project Git remote and access to it, and a configured core
server-access route reachable from the sandbox are required. Agent authentication
is needed to run agent turns. Image builds and running machines incur Modal usage;
this plugin does not provision anything merely by being installed or connected.

## Logo and trademark

The bundled `modal-logo.svg` is an unmodified copy of
[`Modal-IconMark-Dark-OneColor.svg`](https://drive.google.com/file/d/1JvQGLrZsQvnpZu5DmUafxXPGHXDk6TsI/view),
the web one-color icon mark in [Modal's current official brand
assets](https://modal.com/brand). The light one-color file published beside it
uses the same geometry; bb supplies the visible color through its icon mask.

Modal's brand-asset folder publishes no separate license or attribution file.
Modal and its logo are trademarks of Modal Labs, Inc., and Modal's
[terms](https://modal.com/legal/terms) reserve its intellectual-property
rights. The mark remains Modal's property and is bundled only to identify the
service this plugin integrates with; no license to reuse it separately is
granted or implied.


## Debug an image

```sh
bb modal image build --json
bb modal sandbox run --json
bb modal sandbox exec SANDBOX -- bash -lc 'node --version && which git'
bb modal sandbox exec SANDBOX --json -- bash -lc 'exit 7'
bb modal sandbox stop SANDBOX --json
```

Build uses the saved Dockerfile and the same account-wide image cache as machine
creation. It returns the image ID and the final 65,536 characters of build logs
when finished; failures include captured logs and the vendor error. Build logs
are collected through Modal 0.10's gRPC middleware because its image builder does
not forward them. This adapter is tied to the pinned vendor SDK. Output is not
streamed to the CLI. An already submitted build can finish after CLI cancellation.

Run builds or reuses that image and returns `sandboxId`, `imageId`, `expiresAt`
and build `logs`. Debug sandboxes expire after 30 minutes, use configured CPU and
memory, and contain no injected BB credentials, daemon, project clone or setup
hook. They are separate from BB Machines and do not snapshot. Files and running
processes remain between exec calls until stop or expiry. Copy successful fixes
into the Dockerfile, save it, and run a new sandbox to verify them.

Exec passes arguments after `--` literally. Use `bash -lc` for shell expressions.
Place BB's `--json` before `--`; command flags after it belong to the command.
Commands have a 60-second timeout and output is capped at 128 KiB per stream with
a truncation marker. Plain output preserves stdout/stderr and the command exit
code; JSON returns `{exitCode,stdout,stderr}` with the same CLI exit status.
Stopping is idempotent for known debug sandboxes. Exec/stop only accept sandboxes
created by this plugin's debug workflow in the original Modal account; they
cannot target arbitrary sandboxes or provider-managed machines. Stop removes
compute without deleting the shared cached image. Expired IDs remain recognizable.

SDK clients use `sdk.plugins.callRpc` with `modalRpcContract`: `image.build({})`,
`sandbox.run({})`, `sandbox.exec({sandboxId,command})`, and
`sandbox.stop({sandboxId})`. Build/run incur Modal usage.
Loading
Loading