# Box runtime

> SandBoxRuntime remote versus local-docker, default remote, coordinator restart on switch, and loopback-only local gateway ownership.

- Repository: sashimikun/grok-bot-0.18-reconstructed
- GitHub: https://github.com/sashimikun/grok-bot-0.18-reconstructed
- Human docs: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15
- Complete Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/llms-full.txt

## Source Files

- `source/shared/box-runtime.ts`
- `source/electron-main/box/local-docker-host-connector.ts`
- `source/electron-main/box/box-host-connector.ts`
- `source/electron-main/main-edge.ts`
- `source/shared/node/settings/sand-settings-store.ts`

---

---
title: "Box runtime"
description: "SandBoxRuntime remote versus local-docker, default remote, coordinator restart on switch, and loopback-only local gateway ownership."
---

`SandBoxRuntime` is the persisted computer-host mode for Grok Bot. It is a closed enum of `remote` and `local-docker`. The default is `remote`. Settings → Router → Computer exposes a **Use local Docker VM** switch that calls `window.desktop.agent.setBoxRuntime`. On a successful switch, main-edge starts or stops `grok-bot-local-vm`, then `boxRecovery.restartCoordinator()` so the next gateway connect follows the stored mode. Start or stop failure writes the opposite mode back and does not restart the coordinator.

<Note>
Inference routing (`inferenceProvider`) and box runtime (`boxRuntime`) share the Router settings page but are independent. Changing Cursor / Claude Code / Codex / OpenRouter does not change where the computer runs.
</Note>

## Runtime ids

| Id | Default | Computer host | Gateway |
|---|---|---|---|
| `remote` | yes (`DEFAULT_SAND_BOX_RUNTIME`) | Cursor-brokered Sand box, or `SAND_HOST_GATEWAY_URL` when that env is set | Broker `gatewayUrl` (plus token / network token / VNC when the broker supplies them) |
| `local-docker` | no | Owned Docker container `grok-bot-local-vm` | `http://127.0.0.1:1340` with a local bearer token |

`isSandBoxRuntime` accepts only those two strings. Any other `setBoxRuntime` payload fails with `Unknown box runtime.`

## Persistence

`SandStoredSettings` is `settings.json` version `1`. `boxRuntime` is optional. `SandSettingsStore.getBoxRuntime()` returns the stored value or `remote`. `setBoxRuntime` writes the field through the store’s atomic temp-file persist (`settings.json.<pid>.tmp` then rename).

Sibling files next to `settings.json` (same directory):

| Path | Purpose |
|---|---|
| `local-docker-vm.json` | Gateway token (`schemaVersion: 1`, 32-byte hex, mode `0o600`) |
| `local-docker-credential/inference.json` | Optional Cursor inference token for the VM (`accessToken`, `expiresAtMs`, mode `0o600`) |
| `local-docker-runtime/<hostSha256>-<daemonSha256>/` | Content-addressed `host-main.cjs` and `box-exec-daemon/main.cjs` bind mounts |

Invalid JSON, a non-`1` `version`, or a non-enum `boxRuntime` loads as empty settings, so the mode falls back to `remote`.

## Desktop RPC

Both methods require the Sand app window top frame (`createMainEdgeTrust` denial: `The main edge is only accessible from the Sand app window's top frame.`).

Preload surface:

```ts
window.desktop.agent.getBoxRuntime()
window.desktop.agent.setBoxRuntime(mode)
```

Main-edge contract (`source/shared/rpc/main.ts`): `getBoxRuntime` args `none`; `setBoxRuntime` args `object`.

<ParamField body="mode" type="SandBoxRuntime" required>
`remote` or `local-docker`. Required on `setBoxRuntime`.
</ParamField>

<ResponseField name="mode" type="SandBoxRuntime">
Persisted mode after the call. On start/stop failure this is the reverted opposite of the requested mode, and the RPC throws.
</ResponseField>

<ResponseField name="status" type="LocalDockerStatus">
Always the local Docker probe, even when `mode` is `remote`.
</ResponseField>

`LocalDockerStatus`:

| Field | Type | Meaning |
|---|---|---|
| `available` | `boolean` | `docker info` succeeded |
| `running` | `boolean` | Container process is running |
| `ready` | `boolean` | Owned container is running **and** `GET http://127.0.0.1:1340/health` with the stored bearer token returned OK |
| `containerName` | `string` | Always `grok-bot-local-vm` |
| `image` | `string` | Expected `public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest`, or the inspected image if a container already exists |
| `detail` | `string` | Operator-facing status string |

Typical `detail` values: `Docker is not running.`, `Ready to create the local VM.`, `Container grok-bot-local-vm exists but is not owned by Grok Bot.`, `Local Docker VM is ready.`, `Container is starting.`, `Local Docker VM is stopped.`

The packaged Settings Router panel (`RBoxRuntime` from the original-renderer patch) initializes at `{ mode: "remote", status: null, error: null, busy: true }`, then hydrates from `getBoxRuntime`. Toggle copy: remote = `Shell, files and computer use run on Grok Bot's remote computer.`; local = `status.detail` or `Shell, files and computer use run in a Docker container on this Mac.`

## Switch lifecycle

```mermaid
sequenceDiagram
  participant UI as Settings Router RBoxRuntime
  participant Edge as main-edge setBoxRuntime
  participant Store as SandSettingsStore
  participant Docker as startLocalDockerBox / stopLocalDockerBox
  participant Coord as boxRecovery.restartCoordinator

  UI->>Edge: setBoxRuntime({ mode })
  Edge->>Store: persist requested mode
  alt mode is local-docker
    Edge->>Docker: startLocalDockerBox(settingsPath)
  else mode is remote
    Edge->>Docker: stopLocalDockerBox()
  end
  alt start or stop throws
    Edge->>Store: persist opposite mode
    Edge-->>UI: throw (no coordinator restart)
  else start or stop succeeds
    Edge->>Coord: restartCoordinator()
    Coord->>Coord: active.restart()
    Edge-->>UI: { mode, status }
  end
```

Coordinator restart is the same path as `forceReconnectGateway`. Production wiring is `coordinator.restartCoordinator()` → `active?.restart()`. After restart, `createSettingsRoutedHostConnector.connect()` reads `settings.getBoxRuntime()` again:

- `local-docker` → `ensureLocalDockerBox` (single-flight `ensureInFlight`)
- otherwise → the inner remote connector (`BrokeredHostConnector`, or `EnvDescriptorHostConnector` when `SAND_HOST_GATEWAY_URL` is set)

`SAND_HOST_GATEWAY_URL` applies only while the stored mode is `remote`. `local-docker` still owns connect, recreate, and force-recreate.

## Connector routing

```mermaid
flowchart TB
  subgraph ui [Packaged Settings UI]
    Toggle["Use local Docker VM"]
  end
  subgraph rpc [Desktop RPC appWindow]
    Get["getBoxRuntime"]
    Set["setBoxRuntime"]
  end
  subgraph persist [Settings]
    Json["settings.json boxRuntime"]
  end
  subgraph router [createSettingsRoutedHostConnector]
    Route{"getBoxRuntime()"}
  end
  subgraph remote [Remote host]
    Env["EnvDescriptorHostConnector SAND_HOST_GATEWAY_URL"]
    Broker["BrokeredHostConnector ensureSandBox"]
    Fast["Gateway descriptor fast path"]
  end
  subgraph local [Local Docker owner]
    Ctr["grok-bot-local-vm"]
    Gw["127.0.0.1:1340"]
  end
  subgraph coord [Coordinator]
    Restart["restartCoordinator"]
  end
  Toggle --> Set
  Get --> Json
  Set --> Json
  Set --> Restart
  Restart --> Route
  Route -->|"local-docker"| Ctr
  Ctr --> Gw
  Route -->|"remote"| Env
  Route -->|"remote, URL unset"| Broker
  Broker --> Fast
```

Production binds this router in `createProductionCoordinatorGatewayBinding` and in `main-production-services` as `rawRemoteConnector`. Recreate / reset:

| Mode | Recreate | Force recreate |
|---|---|---|
| `remote` | Broker `recreateSandBox`; missing method throws `Remote computer recreation is unavailable.` | Broker `forceRecreateSandBox`; missing method returns `{ status: "rejected", reason: "Remote computer reset is unavailable." }` |
| `local-docker` | `docker restart grok-bot-local-vm`, wait for gateway, `{ status: "started-untrackable" }` | `docker rm --force grok-bot-local-vm` (ignore “no such container”), recreate, `{ status: "started-untrackable" }` |

Local Docker never returns a trackable migration `operationId`. Remote recreate can return `{ status: "started", operationId }` when the broker supplies one.

## Loopback-only local gateway

The local VM is created only when the name `grok-bot-local-vm` is free or already labeled `com.grok-bot.local-vm=1`. Schema version is `"6"`. Published ports bind host loopback only:

`127.0.0.1:1337`, `1339`, `1340`, `6080`, `6081`, `8790`.

Inside the container the gateway binds `SAND_GATEWAY_BIND_HOST=0.0.0.0` and `SAND_HOST_PORT=1340`. Host health checks use the loopback URL, not a LAN address.

:::endpoint GET /health Local Docker gateway readiness
The coordinator treats the VM as ready only when this probe succeeds with the token from `local-docker-vm.json`. Timeout is 2 seconds. Overall start wait is 180 seconds (`Local Docker VM did not expose its gateway within three minutes.`).

```http
GET http://127.0.0.1:1340/health
Authorization: Bearer <token>
```
:::

Ownership and replacement rules:

- Unowned container with the same name: status `ready: false`; start throws `Local Docker VM cannot use grok-bot-local-vm: an unowned container already has that name.`; stop throws `Refusing to stop unowned container grok-bot-local-vm.`
- Unexpected image: start throws; the operator must remove the container before changing images.
- Schema version, `com.grok-bot.local-vm.host-sha256`, or missing inference-credential label when a credential was issued: `docker rm --force` then recreate.
- Stock VM refusal: if reconstructed `host/host-main.cjs` or `box-exec-daemon/main.cjs` cannot be staged, start throws and does not launch the image.

Optional readonly binds when those directories exist: `~/.codex` → `/root/.codex`, `~/.claude` → `/root/.claude`. Named volumes `grok-bot-local-vm-workspace` and `grok-bot-local-vm-data` persist workspace and box data across container replace.

Image, labels, mounts, and health details live on the local Docker connector page. Operator steps (Docker daemon, toggle, rollback) live on Enable the local Docker sandbox.

## Remote connector notes

When mode is `remote` and `SAND_HOST_GATEWAY_URL` is unset, `BrokeredHostConnector.connect()` calls `ensureSandBox`. Empty `gatewayUrl` throws `SandBoxHostConnectError`. Access denials map to the gateway access-denied marker. A `SAND_BOX_BLOCKED` automation hint holds reconnect for `retry-after` or 60 seconds, capped at 15 minutes. `SAND_CLIENT_UPDATE_REQUIRED` notes a backend update requirement. `SAND_HOST_GATEWAY_TOKEN` and `SAND_HOST_GATEWAY_NETWORK_TOKEN` apply only to the env-descriptor path.

## Failure modes

| Condition | Persist | Coordinator | Error / status |
|---|---|---|---|
| Unknown `mode` | unchanged | not restarted | `Unknown box runtime.` |
| Docker daemon missing while selecting `local-docker` | rolled back to `remote` | not restarted | `Local Docker VM is selected, but Docker is unavailable: …` |
| Unowned `grok-bot-local-vm` | rolled back to `remote` on start; stop also refuses | not restarted on throw | Unowned-container errors above |
| Gateway not ready in 3 minutes, or container exits first | rolled back to `remote` | not restarted | Timeout or `Local Docker VM stopped before its gateway became ready.` plus last 80 log lines |
| Stop `local-docker` → `remote` fails | rolled back to `local-docker` | not restarted | `Could not stop the local Docker VM: …` |
| Success | requested mode | `restartCoordinator()` | `{ mode, status }` with `status.ready` reflecting `/health` |

The Settings toggle stays `busy` during the RPC. On throw it restores the previous UI mode and shows `error`.

## Related pages

<CardGroup>
  <Card title="Enable the local Docker sandbox" href="/enable-local-docker">
    Toggle Use local Docker VM, wait for loopback health, and roll back to remote on start failure.
  </Card>
  <Card title="Local Docker connector" href="/docker-sandbox-reference">
    Image, owner labels, schema version 6, published ports, and content-addressed host mounts.
  </Card>
  <Card title="Desktop RPC" href="/desktop-rpc">
    getBoxRuntime, setBoxRuntime, and the rest of window.desktop.agent.
  </Card>
  <Card title="Settings schema" href="/settings-schema">
    settings.json version 1, including boxRuntime and atomic persist.
  </Card>
  <Card title="Router and sandbox failures" href="/router-failures">
    Unowned grok-bot-local-vm and other sandbox start failures.
  </Card>
</CardGroup>
