# @volter/browser-sandboxes

The hosted backend's control-plane side. One sandbox is one host agent from
`@volter/browser-host-node`; this package creates them, drives them, and
exposes them.

- `RemoteRuntime` (`@volter/browser-sandboxes/client.js`) is a browser-safe
  client with the in-page runtime's method names: `spawn`, `start`, `on`,
  `ports`, `portUrl`, `request`, `metrics`, `openPty`, `webSocket`, and an
  asynchronous `files` surface including archive `export` and `import`.
  Passing `terminal` to `start` selects the host's PTY allocator behind that
  same process API; the returned handle streams output and exposes `resize`,
  `result`, and `kill` without a backend-specific launch path.
- `RemoteControlPlane` is the browser-safe lifecycle client: create, inspect,
  pause, resume, snapshot, restore, fork, and destroy, plus a `RemoteRuntime`
  through the plane's token-hiding agent proxy.
- `SandboxManager` creates sandboxes through a provider with four calls,
  `start`, `pause`, `resume`, `stop`, returns a remote runtime per sandbox,
  reaps idle ones from the agent's own activity report, forks workspaces,
  snapshots and restores the portable workspace boundary, and passes
  templates, volumes, and limits through. Snapshot metadata explicitly says
  that process state is excluded.
- `DockerProvider` runs the agent image as a local container.
  `FlyProvider` drives the Fly Machines API, the Fly twin offline or Fly
  itself, reaching machines through flyctl's WireGuard proxy.
- `serveControlPlane` is an HTTP server a page can call that proxies each
  sandbox's agent with the token held server-side.
- `serveMcp` speaks the Model Context Protocol over stdio with tools for
  sandboxes, commands, files, ports, and guest requests.
- `moveWorkspace`, `mirrorWorkspace`, and `returnWorkspace`
  (`@volter/browser-sandboxes/transfer.js`, browser-safe) move a workspace
  into a sandbox without its dependency trees, name the install command its
  lockfile implies, keep the two copies the same while the work runs there,
  and bring the sandbox's work back without the sandbox's tree.

```ts
import { DockerProvider, SandboxManager } from "@volter/browser-sandboxes";

const manager = new SandboxManager({ provider: new DockerProvider({ image: "substrate-node-host:local" }) });
const sandbox = await manager.create();
const result = await sandbox.runtime.spawn("echo $((21 * 2))", { cwd: "/workspace" });
await sandbox.kill();
```

## Run the hosted tier yourself

Everything needed is in this repository under the MIT license.

1. Build the guest image, which carries the kernel and
   `@volter/browser-host-node`: `node examples/node-host/build-image.mjs`
   (add `--platform linux/amd64` for x86 hosts such as Fly Machines).
2. On a machine with Docker, start a control plane with
   `browser-substrate-control-plane`; it creates one container per sandbox
   from that image and proxies each sandbox's agent under
   `/v1/sandboxes/<id>/agent/` with the token held server-side. Point a page
   or the MCP server (`browser-substrate-mcp`) at it.
3. On Fly Machines, push the image to your app's registry and set
   `SUBSTRATE_PROVIDER=fly`, `FLY_APP`, `FLY_IMAGE`, `FLY_REGION`, and
   `FLY_API_TOKEN`; the provider reaches machines through flyctl's WireGuard
   proxy using your existing login. A stopped machine restarts from a clean
   rootfs, so create sandboxes with a `volume` when the workspace must
   survive pause and resume.

## The hybrid page

`examples/node-host/hybrid` is the reference consumer: one page that runs a
project in the tab, decides placement, and moves the workspace to a sandbox
and back through the transfer helper. `npm run dev:hybrid:control-plane`
starts a self-hosted plane on port 4291 and `npm run dev:hybrid` serves the
page on 4292; `HYBRID_PORT` and `CONTROL_PLANE_PORT` give a second copy its
own ports. `CONTROL_PLANE_URL` points the page's proxy at a remote plane
instead, with `CONTROL_PLANE_IP` pinning its address while a name propagates.
Against a managed plane the page's dev server stands in for the adopter's
server: it registers an account, or uses `SUBSTRATE_ACCOUNT_KEY`, passes
`SUBSTRATE_REGISTRATION_CODE` when the plane requires one, and mints a
session per visitor; the tab never holds a key. `verify:node-host:transfer`
with `TRANSFER_PROJECT` naming a project with a lockfile and installed
dependencies measures the move, the far-side install, the mirror, and the
return.

## The wire

The agent speaks `substrate-agent/1` and the control plane
`substrate-control-plane/1`. Each describes itself at its unversioned root
with its protocol, version, base path, route catalogue, and capabilities,
serves every route under `/v1`, carries `x-substrate-protocol` on every
response, and answers 426 to a client stating another version.
`RemoteRuntime` negotiates `GET /` before it trusts the wire and refuses a
server speaking another protocol or version with the reason. The agent's
`/v1` stays visible through the control plane's prefix, so one client class
drives a bare agent and a proxied one alike. `runHostedContract` walks the
catalogue against any live server; a managed service that passes it speaks
the same API as this self-hosted tier.
