diff --git a/apps/server/src/services/plugins/builtin-registry.ts b/apps/server/src/services/plugins/builtin-registry.ts index 3907a93cc3..6a1f50160b 100644 --- a/apps/server/src/services/plugins/builtin-registry.ts +++ b/apps/server/src/services/plugins/builtin-registry.ts @@ -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", diff --git a/apps/server/test/services/plugins/builtin-plugins.test.ts b/apps/server/test/services/plugins/builtin-plugins.test.ts index 4f741b5f8e..6001b4ac7d 100644 --- a/apps/server/test/services/plugins/builtin-plugins.test.ts +++ b/apps/server/test/services/plugins/builtin-plugins.test.ts @@ -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", diff --git a/docs/cli-guide-and-skill.md b/docs/cli-guide-and-skill.md index 9ca2eb42e6..88677efac1 100644 --- a/docs/cli-guide-and-skill.md +++ b/docs/cli-guide-and-skill.md @@ -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. diff --git a/docs/configuration.md b/docs/configuration.md index 413da708f4..84840fa5ab 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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. diff --git a/packages/plugin-build/src/builtin-server-artifacts.test.ts b/packages/plugin-build/src/builtin-server-artifacts.test.ts index 2409ee78fc..5191e1c903 100644 --- a/packages/plugin-build/src/builtin-server-artifacts.test.ts +++ b/packages/plugin-build/src/builtin-server-artifacts.test.ts @@ -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"), ); @@ -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( diff --git a/packages/templates/src/templates/bb-guide-plugins.md b/packages/templates/src/templates/bb-guide-plugins.md index 8a3f216ce3..c64d7f6708 100644 --- a/packages/templates/src/templates/bb-guide-plugins.md +++ b/packages/templates/src/templates/bb-guide-plugins.md @@ -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 `---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. diff --git a/plugins/bb-official.json b/plugins/bb-official.json index 31dc16ea87..867105c24f 100644 --- a/plugins/bb-official.json +++ b/plugins/bb-official.json @@ -141,5 +141,9 @@ "machine-manual": { "category": "environments", "screenshots": [] + }, + "environment-modal-sandbox": { + "category": "environments", + "screenshots": [] } } diff --git a/plugins/environment-modal-sandbox/Dockerfile b/plugins/environment-modal-sandbox/Dockerfile new file mode 100644 index 0000000000..2f345280d3 --- /dev/null +++ b/plugins/environment-modal-sandbox/Dockerfile @@ -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 diff --git a/plugins/environment-modal-sandbox/README.md b/plugins/environment-modal-sandbox/README.md new file mode 100644 index 0000000000..4e5b14a990 --- /dev/null +++ b/plugins/environment-modal-sandbox/README.md @@ -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. diff --git a/plugins/environment-modal-sandbox/account.ts b/plugins/environment-modal-sandbox/account.ts new file mode 100644 index 0000000000..405694014a --- /dev/null +++ b/plugins/environment-modal-sandbox/account.ts @@ -0,0 +1,221 @@ +import { + buildOutput, + runOutput, + execInput, + execOutput, + sandboxInput, + type DebugSandbox, +} from "./debug-sandbox.js"; +import { + defineRpcContract, + type BbPluginApi, + type PluginCliContext, +} from "@get-bb/plugin-sdk"; +import path from "node:path"; +import { z } from "zod"; +import { dockerfileSchema, imageDefinition } from "./image-definition.js"; + +const definitionSchema = z.object({ + dockerfile: z.string(), + customized: z.boolean(), +}); +export const modalRpcContract = defineRpcContract({ + "image.build": { input: z.object({}).strict(), output: buildOutput }, + "sandbox.run": { input: z.object({}).strict(), output: runOutput }, + "sandbox.exec": { input: execInput, output: execOutput }, + "sandbox.stop": { input: sandboxInput, output: sandboxInput }, + "image.definition": { + input: z.object({}).strict(), + output: definitionSchema, + }, + "image.set": { + input: z.object({ dockerfile: dockerfileSchema }).strict(), + output: definitionSchema, + }, + "image.reset": { input: z.object({}).strict(), output: definitionSchema }, + "account.inspect": { + input: z.object({}).strict(), + output: z.object({ available: z.boolean(), message: z.string() }), + }, +}); + +export function registerAccount( + bb: BbPluginApi, + inspect: () => Promise<{ available: boolean; message: string }>, + debug: DebugSandbox, +) { + const image = imageDefinition(bb); + bb.rpc.register(modalRpcContract, { + "image.build": () => debug.build(), + "sandbox.run": () => debug.run(), + "sandbox.exec": (input) => debug.exec(input), + "sandbox.stop": debug.stop, + "account.inspect": inspect, + "image.definition": image.get, + "image.set": ({ dockerfile }) => image.set(dockerfile), + "image.reset": image.reset, + }); + async function readDockerfile(file: string, context: PluginCliContext) { + let hostId: string | undefined; + if (context.threadId) { + const thread = await bb.sdk.threads.get({ threadId: context.threadId }); + if (!thread.environmentId) + throw new Error("The current thread has no machine workspace"); + const environment = await bb.sdk.environments.get({ + environmentId: thread.environmentId, + }); + if (!environment.hostId) + throw new Error("The current thread has no machine workspace"); + hostId = environment.hostId; + } + if (!path.isAbsolute(file) && !context.cwd) + throw new Error("A relative --file requires the CLI working directory"); + const result = await bb.sdk.files.read({ + hostId, + path: path.resolve(context.cwd ?? "/", file), + signal: context.signal, + }); + if (result.contentEncoding !== "utf8") + throw new Error("Dockerfile must be UTF-8 text"); + return dockerfileSchema.parse(result.content); + } + const usage = + "Usage: bb modal account inspect [--json] | bb modal image show [--json] | bb modal image set --file PATH [--json] | bb modal image reset [--json] | bb modal image build [--json] | bb modal sandbox run [--json] | bb modal sandbox exec ID [--json] -- COMMAND... | bb modal sandbox stop ID [--json]"; + bb.cli.register({ + name: "modal", + summary: "Configure, build and debug Modal images", + commands: [ + { + name: "image-build", + summary: "Build or reuse the saved image", + usage: "bb modal image build [--json]", + }, + { + name: "sandbox-run", + summary: "Run the saved image in a 30-minute debug sandbox", + usage: "bb modal sandbox run [--json]", + }, + { + name: "sandbox-exec", + summary: "Execute a command in a debug sandbox", + usage: "bb modal sandbox exec ID [--json] -- COMMAND...", + }, + { + name: "sandbox-stop", + summary: "Stop a debug sandbox", + usage: "bb modal sandbox stop ID [--json]", + }, + { + name: "image-show", + summary: "Show the Dockerfile used for new machines", + usage: "bb modal image show [--json]", + }, + { + name: "image-set", + summary: "Save a Dockerfile for future machines", + usage: "bb modal image set --file PATH [--json]", + }, + { + name: "image-reset", + summary: "Restore the bundled Dockerfile", + usage: "bb modal image reset [--json]", + }, + { + name: "account-inspect", + summary: "Test the configured Modal account", + usage: "bb modal account inspect [--json]", + }, + ], + async run(argv, context) { + try { + const separator = argv.indexOf("--"); + const flags = separator < 0 ? argv : argv.slice(0, separator); + const json = flags.at(-1) === "--json"; + const args = json ? flags.slice(0, -1) : flags; + if (args[0] === "sandbox" && args[1] === "exec") { + if (args.length !== 3 || separator < 0) throw new Error(usage); + const result = await debug.exec( + execInput.parse({ + sandboxId: args[2], + command: argv.slice(separator + 1), + }), + context.signal, + ); + return { + exitCode: result.exitCode, + stdout: json ? JSON.stringify(result) : result.stdout, + stderr: json ? "" : result.stderr, + }; + } + if (separator >= 0) throw new Error(usage); + if (args.length === 2 && args[0] === "image" && args[1] === "build") { + const result = await debug.build(context.signal); + return { + exitCode: 0, + stdout: json + ? JSON.stringify(result) + : `${result.logs}${result.imageId}`, + }; + } + if (args.length === 2 && args[0] === "sandbox" && args[1] === "run") { + const result = await debug.run(context.signal); + return { + exitCode: 0, + stdout: json ? JSON.stringify(result) : result.sandboxId, + stderr: json ? "" : result.logs, + }; + } + if (args.length === 3 && args[0] === "sandbox" && args[1] === "stop") { + const result = await debug.stop( + sandboxInput.parse({ sandboxId: args[2] }), + ); + return { + exitCode: 0, + stdout: json + ? JSON.stringify(result) + : `Stopped ${result.sandboxId}`, + }; + } + if ( + args.length === 2 && + args[0] === "account" && + args[1] === "inspect" + ) { + const result = await inspect(); + return { + exitCode: result.available ? 0 : 1, + stdout: json ? JSON.stringify(result) : result.message, + }; + } + if (args[0] !== "image") throw new Error(usage); + let result; + if (args.length === 2 && args[1] === "show") result = await image.get(); + else if (args.length === 2 && args[1] === "reset") + result = await image.reset(); + else if ( + args.length === 4 && + args[1] === "set" && + args[2] === "--file" && + args[3] + ) + result = await image.set(await readDockerfile(args[3], context)); + else throw new Error(usage); + return { + exitCode: 0, + stdout: json + ? JSON.stringify(result) + : args[1] === "show" + ? result.dockerfile + : args[1] === "reset" + ? "Restored the bundled Dockerfile for future machines." + : "Saved the Dockerfile for future machines.", + }; + } catch (error) { + return { + exitCode: 1, + stderr: error instanceof Error ? error.message : String(error), + }; + } + }, + }); +} diff --git a/plugins/environment-modal-sandbox/app.tsx b/plugins/environment-modal-sandbox/app.tsx new file mode 100644 index 0000000000..e7e12622c5 --- /dev/null +++ b/plugins/environment-modal-sandbox/app.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react"; +import { definePluginApp, useRpc } from "@get-bb/plugin-sdk/app"; +import { Button } from "@bb/shared-ui/button"; +import type { modalRpcContract } from "./account.js"; + +function StandardImage() { + const rpc = useRpc(); + const [saved, setSaved] = useState<{ + dockerfile: string; + customized: boolean; + } | null>(null); + const [dockerfile, setDockerfile] = useState(""); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + useEffect(() => { + let active = true; + void rpc.call("image.definition", {}).then( + (result) => { + if (active) { + setSaved(result); + setDockerfile(result.dockerfile); + } + }, + (failure) => { + if (active) + setError( + failure instanceof Error ? failure.message : String(failure), + ); + }, + ); + return () => { + active = false; + }; + }, [rpc]); + async function save(reset: boolean) { + setSaving(true); + setError(null); + try { + const result = reset + ? await rpc.call("image.reset", {}) + : await rpc.call("image.set", { dockerfile }); + setSaved(result); + setDockerfile(result.dockerfile); + } catch (failure) { + setError(failure instanceof Error ? failure.message : String(failure)); + } finally { + setSaving(false); + } + } + return ( +
+

+ Used for new Modal machines across projects. Supports one FROM followed + by RUN, ENV, WORKDIR, and USER. BB installs its daemon during bootstrap. +

+ {error && ( +

+ {error} +

+ )} + {saved === null ? ( +

+ Loading Dockerfile… +

+ ) : ( + <> +