Skip to content

Commit 68acc13

Browse files
refactor(sandbox): replace sbx runtime with msb driver (#95)
* refactor(sandbox): replace sbx runtime with msb driver * feat(sandbox): in-VM docker, opt-in egress, and plugin-dir installer * feat(loop): per-section checkpoint commits and clean-audit summary re-prompt * chore: bump version to 0.9.0 * feat(sandbox): notify the agent when execution returns to the host Track which sessions resolve to a container so the system prompt gains a one-shot note when a session transitions back to the host, and require the agent to reinstall missing environment tooling instead of excusing it. Resolution is now fail-closed so an unavailable container is not mistaken for a host transition. Session tracking is bounded by an LRU cache. * docs: use pnpm run setup for the source-checkout installer setup is a built-in pnpm command, so the documented pnpm setup --vendor is rejected with "Unknown options: 'vendor'" and bare pnpm setup runs pnpm's own setup instead of the installer. Correct every reference in the README, the configuration guide, and the paths.ts comment, and note why run is required. Regenerating the typedoc output also picks up drift that was already committed: the stale 0.8.9 VERSION, the sandbox max-resource rows, and source links now pointing at HEAD. * fix(session): fail closed on undetermined parent lookup * feat(install): offer to install msb sandbox CLI * feat(sandbox): stream live progress from the template build dialog
1 parent 4fb6558 commit 68acc13

115 files changed

Lines changed: 7430 additions & 2240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
## Generated and bundled files
1616

1717
- `pnpm build` rewrites `src/version.ts`, `src/dashboard/marked-source.ts`, and `src/dashboard/app-bundle.ts`. Edit `package.json`, `src/dashboard/marked.min.js`, or `src/dashboard/app/` respectively, never the generated files.
18-
- The build does not clean `dist/`; remove stale output when deleting or renaming source modules.
18+
- `pnpm build` removes the output directory before compiling (`scripts/build.ts`), so deleting or renaming source modules cannot leave stale output in `dist/`.
19+
- `scripts/build.ts` runs `tsc` first, then `Bun.build` overwrites `dist/index.js` (server) and `dist/tui.js` (TUI) with self-contained bundles. Both must stay bundled so the plugin loads without resolving `node_modules`; the vendored installer mode (`bunx opencode-forge --vendor`) depends on it. `@opentui/*`, `@opencode-ai/plugin/tui`, `solid-js`, and `bun:sqlite` stay external because opencode's runtime provides them.
20+
- `resolveShippedRoot` in `src/utils/shipped-paths.ts` is the only way to locate shipped files on disk. Never derive paths from `import.meta.url` directly: bundling collapses it, which would silently break the migration SQL loader, prompt loading, and bundled-asset sync.
1921
- Bundled prompts (`src/prompts/`) and skills (`skills/`) sync on every plugin load, preserving user edits. The standalone installer handles conflicts and orphan pruning.
20-
- Keep section-summary markers in `src/prompts/agents/auditor-loop-addendum.md` synchronized with constants in `src/utils/section-summary.ts`.
22+
- The section-summary block template is the single `SECTION_SUMMARY_TEMPLATE` in `src/loop/prompts.ts`, built from the marker constants in `src/utils/section-summary.ts`; do not hand-write the marker strings into prompt markdown files or other prompt builders.
2123

2224
## Loop runtime
2325

@@ -29,9 +31,10 @@
2931

3032
## Sandbox
3133

32-
- `src/sandbox/sbx.ts` is the only module invoking the `sbx` CLI; route through its `SandboxRuntime` facade. `src/sandbox/process.ts` is the only child-process spawner; all shell execution goes through `runCommand`.
33-
- `getSandboxState` is the only liveness primitive; four states: `running`, `stopped` (reusable, never create/evict), `unknown` (query failed), `missing` (may create or evict). `registerActiveSandbox` is the only place a usable sandbox is recorded.
34-
- `container/Dockerfile` must derive from `docker.io/docker/sandbox-templates:shell-docker`; no `ENTRYPOINT`, `CMD`, or `WORKDIR`.
34+
- `src/sandbox/msb.ts` is the sole TypeScript runtime/lifecycle facade and `msb` CLI argument owner; route runtime operations through its `SandboxRuntime` facade. The one required exception is the generated shell shim (`src/sandbox/shell-shim.ts`), which invokes `msb exec` directly when an agent shell command must run inside a sandbox. `src/sandbox/process.ts` is the only child-process spawner; all TypeScript shell execution goes through `runCommand`.
35+
- `buildSandboxWorkspaces` canonicalizes the host side of every mount and leaves `containerDir` as the original path. msb refuses a host path that traverses a symlink and fails the whole sandbox with `ENOTDIR`, which on macOS breaks every `os.tmpdir()` mount because `/var` is a symlink to `private/var`. Never canonicalize the container side: absolute paths handed to the agent must resolve identically inside the sandbox.
36+
- `getSandboxState` is the only liveness primitive; five states: `running`, `stopped` (reusable, never create/evict), `transient` (real but not directly executable: `Created`/`Starting`/`Draining`/`Paused` map here), `unknown` (query failed), `missing` (may create or evict). `Stopped`/`Crashed` map to the reusable `stopped` state because `msb exec` starts them in place. `registerActiveSandbox` is the only place a usable sandbox is recorded.
37+
- `container/Dockerfile` must derive from a plain OCI base and keep the final `USER agent`; `ENTRYPOINT`/`CMD` are ignored because msb runs `agentd` as PID 1.
3538

3639
## Dashboard, storage, and paths
3740

‎README.md‎

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ Add to your `opencode.json` to enable Forge’s server-side hooks, tools, and ag
3838
}
3939
```
4040

41+
### Plugin-directory install
42+
43+
Instead of editing the `plugin` arrays by hand, the installer can wire the plugin into opencode's config directory:
44+
45+
```bash
46+
bunx opencode-forge --link # re-export shim for the current build
47+
bunx opencode-forge --vendor # self-contained copy (portable)
48+
```
49+
50+
From a source checkout, use `pnpm run setup --link` or `pnpm run setup --vendor`. Both modes also write the `tui.json` `plugin` entry automatically — opencode does not auto-load the TUI plugin from the plugin directory, so the plugin directory alone cannot enable the sidebar and execution dialog. In a non-interactive shell the flags still require `-y`, `-f`, or `-k`.
51+
52+
| | `--link` | `--vendor` |
53+
| --- | --- | --- |
54+
| Picks up a rebuild | Yes — re-exports the live build | No — re-run after upgrade |
55+
| Portable to another machine | No — absolute path to this checkout | Yes |
56+
| Payload in config dir | Shim only | Full copy (~6 MB) |
57+
| Needs re-run after upgrade | No | Yes |
58+
4159
As of OpenCode 1.17.8, `OPENCODE_EXPERIMENTAL_WORKSPACES=true` is required for the plugin's loop functionality to work. Set it in the environment that launches `opencode`:
4260

4361
```bash
@@ -52,7 +70,7 @@ Forge ships two plugin entrypoints plus standalone management surfaces:
5270

5371
- **Server plugin** — enabled through OpenCode plugin config in `opencode.json`. The package declares the `server` oc-plugin surface and exports `./server` for the server entrypoint.
5472
- **TUI plugin** — enabled separately in `tui.json`. The package declares the `tui` oc-plugin surface and exports `./tui` for the terminal UI entrypoint.
55-
- **Installer CLI** — a standalone CLI accessible via `bunx opencode-forge` or `pnpm setup` (from a source checkout) for installing/upgrading bundled prompts and skills.
73+
- **Installer CLI** — a standalone CLI accessible via `bunx opencode-forge` or `pnpm run setup` (from a source checkout) for installing/upgrading bundled prompts and skills, and for installing the plugin itself into opencode's plugin directory (`--link`/`--vendor`/`--unlink`).
5674
- **Dashboard** — a read-only observability interface launchable from the TUI command palette (`Open dashboard`) or via `pnpm dashboard` (source checkouts only).
5775

5876
The server plugin provides the core hooks, tools, agents, plan storage, loop orchestration, review persistence, and sandbox support. The TUI plugin layers on the sidebar and execution dialog.
@@ -107,11 +125,11 @@ Execution flow dialog with mode and model selection:
107125

108126
- **Plans** — architect authors validated plans directly into SQL storage with `plan-write`/`plan-edit`
109127
- **Execution** — approved-plan launch paths plus direct `/execute-goal` loops in dedicated worktree sessions; plan loops can also target a configured remote opencode server (see [Configuration](docs/configuration.md#remotes)); grouped execution launches features from a PRD as parallel loops
110-
- **Loops** — iterative coding/auditing with isolated git worktree and optional sbx sandbox
128+
- **Loops** — iterative coding/auditing with isolated git worktree and optional msb sandbox
111129
- **Review Findings** — persistent, loop-scoped review findings across loop sessions
112130
- **Group tools** — `launch-group`, `group-status`, `group-cancel` for parallel feature orchestration
113131
- **TUI** — sidebar and execution dialog
114-
- **Sandbox** — Optional sbx worktree loop isolation with bind-mounted project files
132+
- **Sandbox** — Optional msb worktree loop isolation with bind-mounted project files
115133

116134
## Agents
117135

@@ -142,9 +160,9 @@ Forge provides these tool groups:
142160
- **Plan tools** — `plan-write`, `plan-edit`, `plan-read`, `section-read`, `plan-adjust`
143161
- **Review tools** — `review-write`, `review-read`, `review-delete`
144162
- **Loop tools** — `execute-plan`, `execute-goal`, `loop-cancel`, `loop-status`
145-
- **Sandbox routing** — native `bash`, `glob`, and `grep` tools route into sbx for sandboxed sessions
163+
- **Sandbox routing** — native `bash`, `glob`, and `grep` tools route into msb for sandboxed sessions
146164

147-
Loops always run in an isolated git worktree; sbx is used when enabled, configured, and available.
165+
Loops always run in an isolated git worktree; msb is used when enabled, configured, and available.
148166

149167
| Tool | Description |
150168
|------|-------------|
@@ -215,7 +233,7 @@ Flags for non-interactive use:
215233
| `-n`, `--dry-run` | Show what would change without writing anything |
216234
| `--no-prune` | Only report orphaned files; never delete them |
217235

218-
From a checkout, the same tool is available as `pnpm setup` (runs `bun src/install/cli.ts`).
236+
From a checkout, the same tool is available as `pnpm run setup` (runs `bun src/install/cli.ts`). The `run` is required — `setup` is a built-in pnpm command, so `pnpm setup` never reaches this script.
219237

220238
Enable `logging.enabled` to write logs to disk. To use the default log path, omit `logging.file` or set it to `null` (an empty string is not treated as a default). Set `logging.debug` for more verbose output.
221239

@@ -227,7 +245,7 @@ The plugin includes a TUI sidebar widget and an execution dialog for launching p
227245

228246
The sidebar shows Forge's connection status and version. Captured plans live on the server in the `plansRepo` SQL store; the TUI no longer keeps a local archive or in-TUI editor.
229247

230-
When sandboxing is configured, the sidebar displays the current session's sbx state. The `Toggle host sandbox` palette command, and optional `tui.keybinds.toggleHostSandbox` binding, enable or disable sandbox routing for the current session and its Task subagents. The TUI also follows replacement code and auditor sessions when a loop rotates, but does not follow unrelated subagent sessions.
248+
When sandboxing is configured, the sidebar displays the current session's msb state. The `Toggle host sandbox` palette command, and optional `tui.keybinds.toggleHostSandbox` binding, enable or disable sandbox routing for the current session and its Task subagents. The TUI also follows replacement code and auditor sessions when a loop rotates, but does not follow unrelated subagent sessions.
231249

232250
### Additional Commands
233251

@@ -251,7 +269,7 @@ Choose from three execution modes:
251269

252270
1. **New session** — Creates a fresh Code session and sends the plan as the initial prompt
253271
2. **Execute here** — Takes over the current session immediately with the plan
254-
3. **Loop** — Prompts the architect to launch an iterative coding/auditing loop via the `execute-plan` tool in an isolated git worktree (sbx is used when enabled, configured, and available)
272+
3. **Loop** — Prompts the architect to launch an iterative coding/auditing loop via the `execute-plan` tool in an isolated git worktree (msb is used when enabled, configured, and available)
255273

256274
#### Model Selection
257275

@@ -353,7 +371,7 @@ After the architect presents a summary, the user chooses an execution mode from
353371

354372
- **New session** — Creates a new Code session and sends the plan as the initial prompt.
355373
- **Execute here** — The code agent takes over the current session immediately with the plan.
356-
- **Loop** — The architect is prompted to launch an iterative coding/auditing loop via the `execute-plan` tool, which creates an isolated git worktree and provisions sbx when enabled, configured, and available.
374+
- **Loop** — The architect is prompted to launch an iterative coding/auditing loop via the `execute-plan` tool, which creates an isolated git worktree and provisions msb when enabled, configured, and available.
357375

358376
| Mode | When to choose it |
359377
|------|-------------------|
@@ -401,7 +419,7 @@ Loop sessions rotate between code and auditor work, so Forge persists per-sessio
401419

402420
### Worktree Isolation
403421

404-
Loops always run in an isolated git worktree. Sandbox is optional and enabled only when the `sbx` daemon is available and configured (`sandbox.mode = 'sbx'`): when available, a sandbox is provisioned automatically alongside the worktree; otherwise the loop runs in worktree-only mode. Changes are auto-committed and the worktree is removed on completion (branch preserved for later merge).
422+
Loops always run in an isolated git worktree. Sandbox is optional and controlled by `sandbox.enabled` (default `true`) with driver `sandbox.mode = 'msb'`: when enabled, a sandbox is provisioned automatically alongside the worktree. If the `msb` CLI is missing or the host cannot run microVMs, sandbox startup fails and the loop start is rolled back — it never silently falls back to the host. Set `sandbox.enabled: false` to run worktree-only. Changes are auto-committed and the worktree is removed on completion (branch preserved for later merge).
405423

406424
### Auditor Integration
407425

@@ -490,7 +508,7 @@ All worktree-based execution paths require a git repository with at least one ro
490508
When a worktree loop starts with `OPENCODE_EXPERIMENTAL_WORKSPACES=true`, forge:
491509

492510
1. Calls `experimental.workspace.create` with `type: "forge"`, `branch: null`, and `extra: { loopName, projectDirectory, workspaceCreatedAt }` to register the workspace through the `forge` adapter
493-
2. The adapter's `create` hook creates the git worktree (reusing an orphaned branch when possible) and, when configured, provisions the sbx sandbox
511+
2. The adapter's `create` hook creates the git worktree (reusing an orphaned branch when possible) and, when configured, provisions the msb sandbox
494512
3. Creates a new Code session pointed at the worktree directory
495513
4. Calls `experimental.workspace.warp` to bind the session to that workspace
496514
5. Persists the workspace ID on the loop record (`loops.workspace_id`) so the TUI can route clicks on a loop into the correct workspace
@@ -526,29 +544,29 @@ Worktree loops require a git repository with at least one commit. OpenCode scope
526544

527545
## Sandbox
528546

529-
Run loop iterations inside an isolated `sbx` sandbox when the `sbx` daemon is available and configured. Sandbox is optional: when `sbx` is enabled, Forge provisions a loop sandbox automatically; otherwise loops run in worktree-only mode.
547+
Run loop iterations inside an isolated `msb` sandbox. Sandbox is optional and controlled by `sandbox.enabled` (default `true`) with driver `sandbox.mode = 'msb'`: when enabled, Forge provisions a loop sandbox automatically. If the `msb` CLI is unavailable or the host cannot run microVMs, sandbox startup fails and the loop is rolled back rather than silently falling back to the host; set `sandbox.enabled: false` to run worktree-only.
530548

531-
See [Sandbox](docs/sandbox.md) for setup, native in-sandbox Docker, network access, environment passthrough, custom bind mounts, large-output handling, and resource defaults.
549+
See [Sandbox](docs/sandbox.md) for setup, host requirements, image building and loading, network access, environment passthrough and secrets, custom bind mounts, large-output handling, and resource defaults.
532550

533551
### Prerequisites
534552

535-
- The `sbx` CLI installed and authenticated (`sbx login`), with the `sbx` daemon running (`sbx daemon start`) on a supported platform (macOS 14+ Apple silicon, Windows 11 with Hypervisor Platform, or Ubuntu 24.04+ with KVM).
536-
- Docker, used only to build the sandbox template.
553+
- The `msb` CLI installed — no account or login step. Install with `curl -fsSL https://install.microsandbox.dev | sh` and verify with `msb doctor` on a supported platform (Linux with KVM, macOS on Apple silicon, or Windows 11 with Windows Hypervisor Platform). The interactive installer offers to run this for you when `msb` is not on `PATH`.
554+
- Docker, required on the host only to build the sandbox image (the msb runtime itself does not need it; Docker *inside* the sandbox is a separate in-image stack).
537555
- OpenCode >= 1.15.5 — sandbox shell routing relies on the session-aware `shell.env` plugin hook. Enforced via `engines.opencode`, so older versions refuse to load the plugin rather than silently running sandbox commands on the host. (Loops additionally require OpenCode >= 1.17.8 for workspace integration, see [Requirements](#requirements).)
538556

539557
### Setup
540558

541-
**1. Build and load the sandbox template:**
559+
**1. Build and load the sandbox image:**
542560

543561
```bash
544562
docker build -t oc-forge-sandbox:latest container/
545563
docker save oc-forge-sandbox:latest -o forge-sandbox.tar
546-
sbx template load forge-sandbox.tar
564+
msb load --input forge-sandbox.tar --tag oc-forge-sandbox:latest
547565
```
548566

549-
The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git, and jq. Chromium and Browser Control are an opt-in image feature: set `sandbox.imageFeatures.browserControl` to `true`, then run `Build sandbox template` from the command palette to rebuild and load the configured image tag.
567+
The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a full Docker Engine (docker-ce, CLI, containerd, Buildx, and Compose from Docker's official apt repo) that runs natively inside the microVM — `docker run`, `docker build`, and `docker compose` all work in-sandbox. The daemon is started on demand by `forge-dockerd-start` (msb boots its own `agentd` as PID 1 and ignores the image's entrypoint, so nothing runs dockerd at boot); `/var/lib/docker` is backed by a dedicated block device because overlayfs cannot run on a virtiofs mount. The built image is roughly 1.65 GB. Chromium and Browser Control are an opt-in image feature: set `sandbox.imageFeatures.browserControl` to `true`, then run `Build sandbox template` from the command palette to rebuild and load the configured image tag.
550568

551-
The `container/Dockerfile` ships with the plugin package. If the template is missing when OpenCode starts, Forge shows a warning toast with a "Build sandbox template" command in the palette. You can also trigger the build from the command palette at any time by searching for `Build sandbox template`, which opens a confirmation dialog and runs the build/save/load sequence automatically.
569+
The `container/Dockerfile` ships with the plugin package. If the image is missing when OpenCode starts, Forge shows a warning toast with a "Build sandbox template" command in the palette. You can also trigger the build from the command palette at any time by searching for `Build sandbox template`, which opens a confirmation dialog and runs the build/save/load sequence automatically. The dialog stays open for the duration and shows a live progress bar, the current Docker step, elapsed time, and streamed build output; on failure it keeps the last lines of Docker output so the cause is visible. A first build takes several minutes. Closing the dialog does not cancel the build — it finishes in the background and reports with a toast.
552570

553571
Restart OpenCode after changing sandbox configuration.
554572

0 commit comments

Comments
 (0)