# Local Docker connector

> Image, container name, owner label, schema version 6, loopback gateway URL, health probe, and content-addressed host-main.cjs mounts.

- 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/electron-main/box/local-docker-host-connector.ts`
- `source/shared/box-runtime.ts`
- `source/electron-main/box/box-host-connector.ts`
- `source/electron-main/main-edge.ts`
- `scripts/build-box-exec-daemon.mjs`
- `source/box-exec-daemon/main.ts`

---

---
title: "Local Docker connector"
description: "Image, container name, owner label, schema version 6, loopback gateway URL, health probe, and content-addressed host-main.cjs mounts."
---

`createSettingsRoutedHostConnector` in `source/electron-main/box/local-docker-host-connector.ts` owns the local sandbox. When `settings.json` stores `boxRuntime: "local-docker"`, `connect()` starts or reuses Docker container `grok-bot-local-vm`, mounts reconstructed `host-main.cjs` and `box-exec-daemon/main.cjs` from a content-addressed host directory, and returns gateway `{ baseUrl: "http://127.0.0.1:1340", token }`. The default runtime remains `"remote"` (`DEFAULT_SAND_BOX_RUNTIME`).

<Note>
This page is the connector contract. Toggle, rollback, and coordinator restart live on [Enable the local Docker sandbox](/enable-local-docker) and [Box runtime](/box-runtime).
</Note>

## Constants

| Identifier | Value |
|---|---|
| `LOCAL_DOCKER_BOX_IMAGE` | `public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest` |
| `LOCAL_DOCKER_BOX_CONTAINER` | `grok-bot-local-vm` |
| `LOCAL_DOCKER_OWNER_LABEL` | `com.grok-bot.local-vm=1` |
| `LOCAL_DOCKER_SCHEMA_VERSION` | `"6"` |
| `LOCAL_DOCKER_GATEWAY_URL` | `http://127.0.0.1:1340` |
| Ready timeout | `180_000` ms |
| Optional inference credential wait | `3_000` ms |
| Health probe timeout | `2_000` ms |
| Docker platform | `linux/amd64` |
| Restart policy | `unless-stopped` |

Owner inspection reads Docker label `com.grok-bot.local-vm` and requires `"1"`. Schema inspection reads `com.grok-bot.local-vm.schema-version`.

<Warning>
A container named `grok-bot-local-vm` without that owner label is refused. The connector will not start, stop, or replace an unowned name collision.
</Warning>

## Architecture

```mermaid
flowchart TB
  subgraph desktop [Electron main]
    RPC["getBoxRuntime / setBoxRuntime"]
    Routed["createSettingsRoutedHostConnector"]
    Remote["createRemoteHostConnector"]
    Stage["stageCurrentHostBundle"]
  end
  subgraph hostfs [Directory next to settings.json]
    TokenFile["local-docker-vm.json"]
    RuntimeDir["local-docker-runtime/hostSha-daemonSha/"]
    InferFile["local-docker-credential/inference.json"]
  end
  subgraph docker [Docker daemon]
    Ctr["grok-bot-local-vm"]
    VolWs["volume grok-bot-local-vm-workspace"]
    VolData["volume grok-bot-local-vm-data"]
  end
  subgraph box [Container linux/amd64]
    Host["/home/box/sand-host/host-main.cjs :1340"]
    Daemon["/home/box/box-exec-daemon :1337"]
    Workspace["/workspace"]
    SandData["/home/box/sand-data"]
  end
  RPC --> Routed
  Routed -->|"boxRuntime local-docker"| Stage
  Routed -->|"boxRuntime remote"| Remote
  Stage --> RuntimeDir
  Routed --> TokenFile
  Routed --> InferFile
  Routed -->|"docker run / start / rm"| Ctr
  RuntimeDir -->|"readonly bind"| Host
  RuntimeDir -->|"readonly bind"| Daemon
  VolWs --> Workspace
  VolData --> SandData
  Host -->|"GET /health Bearer"| Routed
```

`createRemoteHostConnector` still brokers a remote box when `boxRuntime` is `"remote"`, including `SAND_HOST_GATEWAY_URL` env override and the Cursor `ensureSandBox` path. The local connector wraps that object; it does not replace remote recreation APIs until the stored mode is `"local-docker"`.

## Status payload

`getLocalDockerStatus(settingsPath)` runs `docker info` then `docker inspect grok-bot-local-vm`.

<ResponseField name="available" type="boolean">
`true` when `docker info` succeeds.
</ResponseField>

<ResponseField name="running" type="boolean">
`State.Running === true` from inspect.
</ResponseField>

<ResponseField name="ready" type="boolean">
`true` only when the container is owned, running, and `GET http://127.0.0.1:1340/health` returns HTTP OK with `Authorization: Bearer <token>`.
</ResponseField>

<ResponseField name="containerName" type="string">
Always `grok-bot-local-vm`.
</ResponseField>

<ResponseField name="image" type="string">
Expected image when the container does not exist; otherwise the inspected `Config.Image`.
</ResponseField>

<ResponseField name="detail" type="string">
Operator string from the table below.
</ResponseField>

| Condition | `available` | `running` | `ready` | `detail` |
|---|---|---|---|---|
| Docker CLI/daemon missing | `false` | `false` | `false` | Docker error text, or `Docker is not running.` |
| No container | `true` | `false` | `false` | `Ready to create the local VM.` |
| Name exists, label missing | `true` | inspect | `false` | `Container grok-bot-local-vm exists but is not owned by Grok Bot.` |
| Owned, health OK | `true` | `true` | `true` | `Local Docker VM is ready.` |
| Owned, running, health not OK | `true` | `true` | `false` | `Container is starting.` |
| Owned, stopped | `true` | `false` | `false` | `Local Docker VM is stopped.` |

Malformed inspect JSON throws `Docker returned malformed container inspection data.`

## Desktop RPC

`window.desktop.agent.getBoxRuntime` / `setBoxRuntime` in `source/electron-main/main-edge.ts` persist `boxRuntime` then call this connector.

<ParamField body="mode" type='"remote" \| "local-docker"' required>
Unknown values throw `Unknown box runtime.`
</ParamField>

On `setBoxRuntime`:

1. Persist `mode`.
2. `"local-docker"` → `startLocalDockerBox(settingsPath)` (no inference credential).
3. `"remote"` → `stopLocalDockerBox()`.
4. Failure rolls `boxRuntime` back to the other mode and rethrows.
5. Success restarts the coordinator, then returns `{ mode, status }`.

Coordinator `connect()` uses the settings-routed wrapper, which *can* pass an optional inference credential (3s race against `issueInferenceCredential`). A toggle-started container without that label is replaced on the first successful credentialed connect.

## Ensure, start, stop, recreate

```text
inspect grok-bot-local-vm
        │
        ├─ missing Docker ────────────── throw
        ├─ exists, unowned ──────────── throw (name collision)
        ├─ exists, image ≠ expected ─── throw (explicit rm required)
        ├─ exists, schema/host SHA /
        │   missing inference label ── docker rm --force, then create
        ├─ exists, stopped ──────────── docker start
        └─ missing ──────────────────── docker run --detach
                │
                └─ poll GET /health until 180s or container exit
```

`startLocalDockerBox(settingsPath)` is `ensureLocalDockerBox` without a credential.

`stopLocalDockerBox()` returns immediately if the container is missing or already stopped. Unowned running containers throw `Refusing to stop unowned container grok-bot-local-vm.`

Local `recreate` runs `docker restart grok-bot-local-vm`, then `localConnect()`, and returns `{ status: "started-untrackable" }`.

Local `forceRecreate` runs `docker rm --force` (ignores “no such container”), then `localConnect()`, same untrackable status. Other `rm` failures return `{ status: "rejected", reason }`.

Concurrent `connect()` calls share one in-flight `ensureLocalDockerBox` promise.

## Gateway health

:::endpoint GET /health Loopback sandbox ready probe
The connector treats the box as ready when this URL returns HTTP OK.

**URL:** `http://127.0.0.1:1340/health`

**Headers:** `authorization: Bearer <token>` from `local-docker-vm.json`

**Timeout:** 2 seconds (`AbortSignal.timeout(2_000)`)

The reconstructed host serves `GET /health` *before* bearer checks and returns JSON:

```json
{
  "ok": true,
  "pid": 1,
  "isBusy": false,
  "activeAgentId": null,
  "startedAt": 0,
  "lastBusyAtMs": null
}
```

Readiness for the connector is `response.ok`, not a field-by-field schema check. Browser `Origin` requests are rejected by the host gateway (`403 browser-origin gateway requests are not allowed`).
:::

Inside the container the host binds `SAND_GATEWAY_BIND_HOST=0.0.0.0` and `SAND_HOST_PORT=1340` so the published loopback port can reach it. The host still requires the pinned `SAND_GATEWAY_TOKEN` for `/api/*` and other authenticated routes.

If the container exits during the wait, the connector tails `docker logs --tail 80 grok-bot-local-vm` into `Local Docker VM stopped before its gateway became ready.` If the deadline elapses: `Local Docker VM did not expose its gateway within three minutes.`

## Content-addressed host mounts

`stageCurrentHostBundle` refuses a stock image host. It reads reconstructed bytes from paths relative to the packaged Electron-main module:

- `host/host-main.cjs`
- `box-exec-daemon/main.cjs`

Candidates are `../` then `../../` from `source/electron-main/box/`. Missing both throws `The reconstructed runtime is unavailable at …; refusing to start a stock local VM.`

SHA-256 of each file names the staging directory next to `settings.json`:

```text
<settingsDir>/local-docker-runtime/<hostSha256>-<boxExecDaemonSha256>/
  host-main.cjs
  box-exec-daemon/main.cjs
```

Writes use `0o600`, atomic `rename`, and reject an existing file whose bytes do not match (`Content-addressed local runtime … has unexpected bytes.`).

Bind mounts on `docker run`:

| Host path | Container path | Mode |
|---|---|---|
| staged `host-main.cjs` | `/home/box/sand-host/host-main.cjs` | readonly |
| staged `box-exec-daemon/` directory | `/home/box/box-exec-daemon` | readonly |
| `<settingsDir>/local-docker-credential/` (if a credential was issued) | `/run/grok-bot` | readonly |
| `$HOME/.codex` if it is a directory | `/root/.codex` | readonly |
| `$HOME/.claude` if it is a directory | `/root/.claude` | readonly |

Named volumes (survive `docker rm` of the container):

| Volume | Mount |
|---|---|
| `grok-bot-local-vm-workspace` | `/workspace` |
| `grok-bot-local-vm-data` | `/home/box/sand-data` |

`scripts/build-box-exec-daemon.mjs` bundles `source/box-exec-daemon/cli.ts` to CJS (`platform: node`, `target: node22`) with banner `Reconstructed loopback box exec-daemon; Connect/protobuf transport, no desktop local-exec dependency.` The daemon listens on `127.0.0.1:1337` by default (`BOX_EXEC_DAEMON_HOST` / `BOX_EXEC_DAEMON_PORT`) and requires `Authorization: Bearer` (default token `local`, overridable with `SAND_BOX_EXEC_DAEMON_AUTH_TOKEN`).

The container sets `SAND_USE_EXISTING_BOX_EXEC_DAEMON=1` so in-box `host-main.cjs` does **not** spawn a second daemon process; it uses the mounted artifact. `SAND_BOX_AUTO_UPDATE=0` disables host-bundle auto-update. `SAND_SUPERVISOR_ENABLED=1` stays on.

## Container labels and replacement

`docker run` stamps:

| Label | Value |
|---|---|
| `com.grok-bot.local-vm` | `1` |
| `com.grok-bot.local-vm.host-sha256` | SHA-256 of staged `host-main.cjs` |
| `com.grok-bot.local-vm.box-exec-daemon-sha256` | SHA-256 of staged `box-exec-daemon/main.cjs` |
| `com.grok-bot.local-vm.inference-credential` | `"1"` or `"0"` |
| `com.grok-bot.local-vm.schema-version` | `"6"` |

Force-replace (`docker rm --force` then create) when any of:

- inspected schema ≠ `"6"`
- inspected host SHA-256 ≠ current `host-main.cjs` SHA-256
- this ensure pass has an inference credential **and** the container label is not `"1"`

Daemon SHA-256 is recorded on the container but is **not** a replace predicate. Changing only `box-exec-daemon/main.cjs` does not recycle an existing schema-6 container with a matching host SHA.

Changing images is never automatic: a live container whose `Config.Image` is not `LOCAL_DOCKER_BOX_IMAGE` throws `Local Docker VM container uses unexpected image … Remove it explicitly before changing images.`

## Loopback ports

All publishes bind `127.0.0.1` only:

| Host port | In-box service |
|---|---|
| `1337` | Box exec daemon (`EXEC_DAEMON_PORT` / `BOX_EXEC_DAEMON_PORT`) |
| `1339` | Fork desktop router (`SAND_BOX_FORK_ROUTER_PORT`); the reconstructed standalone daemon does not advertise fork-window start |
| `1340` | Host gateway (`SAND_HOST_PORT`) |
| `6080` | Primary noVNC (`SAND_BOX_PRIMARY_NOVNC_PORT`) |
| `6081` | Fork noVNC (`SAND_BOX_FORK_NOVNC_PORT`) |
| `8790` | Egress-tunnel WebSocket (`EGRESS_TUNNEL_WS_PORT`) |

Do not confuse `grok-bot-local-vm` with the separate dev-controls default container name `sand-dev-box`.

## Gateway token and inference files

Token file `<settingsDir>/local-docker-vm.json`:

```json
{
  "schemaVersion": 1,
  "token": "<64 hex chars>"
}
```

Reuse requires `token` length ≥ 32. Otherwise the connector writes `randomBytes(32).toString("hex")` at mode `0o600`. That value is `SAND_GATEWAY_TOKEN` in the container and the Bearer used for health and later coordinator `GatewayConnection.token`.

Optional inference file `<settingsDir>/local-docker-credential/inference.json` (mode `0o600`):

```json
{
  "accessToken": "…",
  "expiresAtMs": 0
}
```

When present, the container also gets `SAND_DEV_INFERENCE_TOKEN_FILE=/run/grok-bot/inference.json` and `SAND_BACKEND_URL=<issued backendUrl>`. `backendUrl` is not stored in the JSON file. The 3s credential race is best-effort: timeout yields `undefined` and the VM starts without that mount.

## Environment inside the container

| Name | Value |
|---|---|
| `SAND_SUPERVISOR_ENABLED` | `1` |
| `SAND_BOX_AUTO_UPDATE` | `0` |
| `SAND_USE_EXISTING_BOX_EXEC_DAEMON` | `1` |
| `SAND_TREE_SITTER_NODE_DEPS` | `/home/box/deps` |
| `NODE_PATH` | `/home/box/deps` |
| `SAND_GATEWAY_BIND_HOST` | `0.0.0.0` |
| `SAND_HOST_PORT` | `1340` |
| `SAND_GATEWAY_TOKEN` | token from `local-docker-vm.json` |
| `SAND_DEV_INFERENCE_TOKEN_FILE` | `/run/grok-bot/inference.json` (credentialed start only) |
| `SAND_BACKEND_URL` | issued backend URL (credentialed start only) |

`source/box-exec-daemon/main.ts` additionally honors `SAND_BOX_WORKSPACE_ROOT`, `SAND_BOX_EXEC_DAEMON_PORT`, `SAND_BOX_TERMINALS_DIRECTORY`, and `SAND_BOX_EXEC_DAEMON_AUTH_TOKEN` when the mounted daemon process starts.

## Errors

| Failure | Message / behavior |
|---|---|
| Docker unavailable | `Local Docker VM is selected, but Docker is unavailable: …` |
| Unowned name | `Local Docker VM cannot use grok-bot-local-vm: an unowned container already has that name.` |
| Wrong image | Unexpected image; operator must `docker rm` first |
| Replace `rm` failed | `Could not replace the local VM with the current app runtime: …` |
| `docker start` failed | `Could not start the local Docker VM: …` |
| `docker run` failed | `Could not create the local Docker VM: …` |
| `docker stop` failed | `Could not stop the local Docker VM: …` |
| Unowned stop | `Refusing to stop unowned container grok-bot-local-vm.` |
| Missing reconstructed CJS | Refuses stock VM |
| Staging byte mismatch | Content-addressed path has unexpected bytes |
| Health never OK | Three-minute timeout, or logs if the container died |
| `setBoxRuntime` start failure | Persist rolled back to `"remote"` |
| `setBoxRuntime` stop failure | Persist rolled back to `"local-docker"` |

`publication-packaging` tests pin the image registry string, `127.0.0.1:1340:1340`, `SAND_BOX_AUTO_UPDATE=0`, the readonly `host-main.cjs` destination, and settings-routed `connect()` branching.

## Next

<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="Box runtime" href="/box-runtime">
    `remote` versus `local-docker`, default remote, and coordinator restart on switch.
  </Card>
  <Card title="Desktop RPC" href="/desktop-rpc">
    `getBoxRuntime` and `setBoxRuntime` on `window.desktop.agent`.
  </Card>
  <Card title="Router and sandbox failures" href="/router-failures">
    Unowned `grok-bot-local-vm` and other sandbox/router abort paths.
  </Card>
  <Card title="Settings schema" href="/settings-schema">
    `settings.json` version 1 field `boxRuntime`.
  </Card>
</CardGroup>
