# Grok Bot 0.18 Reconstructed Documentation

> Technical documentation for the unofficial Grok Bot 0.18 reconstruction: pinned bootstrap, reconstructed Electron/host/coordinator runtimes, inference routing, local Docker sandbox, and macOS packaging.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15)
- [GitHub repository](https://github.com/sashimikun/grok-bot-0.18-reconstructed)

## Repository Metadata

- Repository: sashimikun/grok-bot-0.18-reconstructed

- Generated: 2026-08-24T05:59:56.688Z
- Updated: 2026-08-24T06:02:19.192Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 26

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/01-overview.md) - What this reconstruction exposes, who can run it, the hybrid runtime split, and the first docs routes after clone.
- 02. [Installation](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/02-installation.md) - macOS Apple Silicon prerequisites, Node 26.5.x, Git LFS, Xcode tools, optional Docker, and npm ci.
- 03. [Quickstart](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/03-quickstart.md) - Clone, LFS pull, bootstrap, check, package, and open dist/Grok Bot 0.18 Reconstructed.app with the expected success signal.
- 04. [Reconstruction boundary](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/04-reconstruction-boundary.md) - Compiled source/ runtimes, checksum-pinned shipped renderer, reconstructed bundle id com.anysphere.sand.reconstructed, and ignored src/app/dist hydration.
- 05. [Inference router](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/05-inference-router.md) - SandInferenceProvider ids cursor, claude-code, codex, openrouter; default cursor; local transcript store schemaVersion 2; usage schemaVersion 1.
- 06. [Box runtime](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/06-box-runtime.md) - SandBoxRuntime remote versus local-docker, default remote, coordinator restart on switch, and loopback-only local gateway ownership.
- 07. [Evidence and provenance](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/07-evidence-and-provenance.md) - Pinned 0.18.0 DMG and app.asar SHA-256 identities, evidence-only UI rule, and production bindings that must resolve into source/.
- 08. [Choose an inference provider](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/08-choose-an-inference-provider.md) - Set Settings → Router, persist inferenceProvider, and satisfy Cursor session, Claude Code login, Codex auth.json, or OPENROUTER_API_KEY.
- 09. [Enable the local Docker sandbox](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/09-enable-the-local-docker-sandbox.md) - Toggle Use local Docker VM, start grok-bot-local-vm, wait for http://127.0.0.1:1340/health, and roll back to remote on start failure.
- 10. [Route Grok Bot plugin tools](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/10-route-grok-bot-plugin-tools.md) - listRoutedMcpTools and executeRoutedMcpTool across Claude Code MCP bridge versus Codex/OpenRouter direct tool execution.
- 11. [Bootstrap the pinned runtime](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/11-bootstrap-the-pinned-runtime.md) - npm run bootstrap order: GROK_BOT_018_APP, cached app, LFS DMG, then public URL; verify DMG and app.asar SHA-256; hydrate src/app/dist.
- 12. [Package the macOS app](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/12-package-the-macos-app.md) - npm run package compiles runtimes, patches Settings Router, replaces app.asar, sets reconstructed identity, ad-hoc signs, and verifies.
- 13. [Work on the frontend reconstruction](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/13-work-on-the-frontend-reconstruction.md) - frontend/ is a Vite design workspace; packaged UI stays the patched shipped renderer unless GROK_BOT_RENDERER_SOURCE overrides it.
- 14. [npm scripts](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/14-npm-scripts.md) - Exact package.json commands: bootstrap, check, package, verify, smoke, publication:check, frontend:build, and test globs.
- 15. [Settings schema](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/15-settings-schema.md) - settings.json version 1 fields: inferenceProvider, inferenceRouterUsage, boxRuntime, MCP instruction maps, and atomic persist path.
- 16. [Environment variables](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/16-environment-variables.md) - GROK_BOT_018_APP, GROK_BOT_OUTPUT_APP_NAME, SAND_DISABLE_*, OPENROUTER_API_KEY, SAND_OPENROUTER_MODEL, CODEX_HOME, CLAUDE_CODE_PATH, and binding-manifest overrides.
- 17. [Desktop RPC](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/17-desktop-rpc.md) - window.desktop.agent getInferenceRouter, setInferenceRouter, getBoxRuntime, setBoxRuntime, and secrets upsert/list channels.
- 18. [Local Docker connector](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/18-local-docker-connector.md) - Image, container name, owner label, schema version 6, loopback gateway URL, health probe, and content-addressed host-main.cjs mounts.
- 19. [Provider clients](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/19-provider-clients.md) - Cursor default path, Claude Agent SDK query, Codex Responses at chatgpt.com/backend-api/codex/responses, OpenRouter OpenAI-compatible model default openai/gpt-5.2.
- 20. [Bootstrap failures](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/20-bootstrap-failures.md) - LFS pointer-sized archives, DMG checksum mismatch, missing app.asar files, GROK_BOT_018_APP version mismatch, and hydration checksum errors.
- 21. [Packaging failures](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/21-packaging-failures.md) - Non-darwin package abort, missing renderer patch anchors, ad-hoc codesign retry, xattr quarantine, and verify required ASAR entries.
- 22. [Router and sandbox failures](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/22-router-and-sandbox-failures.md) - Unknown provider, Codex private auth.json, Claude Code not installed, missing OPENROUTER_API_KEY, and unowned grok-bot-local-vm container.
- 23. [Verify and smoke](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/23-verify-and-smoke.md) - npm run verify required ASAR paths and evidence markers; npm run smoke native e2e with 12s timeout; updater guard SAND_DISABLE_* defaults.
- 24. [Preserve original installers](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/24-preserve-original-installers.md) - Git LFS 0.18.0 macOS DMG and Windows setup.exe identities, artifacts.json schemaVersion 1, and shasum -c SHA256SUMS.
- 25. [Publication export](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/25-publication-export.md) - npm run publication:check archive/init/add tree equality, LFS push of installers, and NOTICE/rights review before a public remote.
- 26. [Contributing](https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/26-contributing.md) - Required check and package commands, no generated payloads in Git, do not weaken checksum or signing gates, private security reports.

## Source File Index

- `CONTRIBUTING.md`
- `docs/ARCHITECTURE.md`
- `docs/PUBLISHING.md`
- `frontend/README.md`
- `frontend/src/recovered/features/settings/overlay/router.ts`
- `frontend/src/recovered/features/settings/overlay/view.tsx`
- `frontend/vite.config.ts`
- `manifests/reconstruction/electron-main-production-bindings-manifest.json`
- `NOTICE.md`
- `package.json`
- `PROVENANCE.md`
- `README.md`
- `research-archives/original/0.18.0/artifacts.json`
- `research-archives/original/0.18.0/SHA256SUMS`
- `research-archives/README.md`
- `scripts/apply-third-party-patches.mjs`
- `scripts/audit-ui-provenance.mjs`
- `scripts/bootstrap-runtime.mjs`
- `scripts/build-box-exec-daemon.mjs`
- `scripts/build.mjs`
- `scripts/clean-build.mjs`
- `scripts/lib/build-asar.mjs`
- `scripts/lib/codesign.mjs`
- `scripts/lib/config.mjs`
- `scripts/lib/macos-package-verification.mjs`
- `scripts/lib/router-renderer-patch.mjs`
- `scripts/lib/runtime.mjs`
- `scripts/lib/system-tools.mjs`
- `scripts/native-e2e-check.mjs`
- `scripts/package-macos.mjs`
- `scripts/recover-frontend.mjs`
- `scripts/smoke.mjs`
- `scripts/verify-publication-tree.mjs`
- `scripts/verify.mjs`
- `SECURITY.md`
- `source/box-exec-daemon/main.ts`
- `source/electron-main/box/box-host-connector.ts`
- `source/electron-main/box/local-docker-host-connector.ts`
- `source/electron-main/coordinator/coordinator-executors.ts`
- `source/electron-main/main-edge.ts`
- `source/electron-main/main.ts`
- `source/electron-main/secrets/secrets-ipc.ts`
- `source/electron-preload/main-rpc-runtime.ts`
- `source/electron-preload/preload.ts`
- `source/host/extensions/inference/codex-direct-responses.ts`
- `source/host/extensions/inference/cursor-session.ts`
- `source/host/extensions/inference/provider-session.ts`
- `source/host/extensions/mcp/mcp-service.ts`
- `source/host/extensions/settings/settings-service.ts`
- `source/host/main.ts`
- `source/node-agent-coordinator/inference-router.ts`
- `source/node-agent-coordinator/main.ts`
- `source/node-agent-coordinator/routed-mcp-bridge.ts`
- `source/shared/box-runtime.ts`
- `source/shared/host-settings.ts`
- `source/shared/inference-router.ts`
- `source/shared/local-tool-permission.ts`
- `source/shared/node/inference-router-local.ts`
- `source/shared/node/settings/sand-settings-store.ts`
- `source/shared/rpc/coordinator.ts`
- `source/shared/rpc/main.ts`
- `src/app/package.json`
- `tests/backend-mcp-exec-json.test.mjs`
- `tests/codex-direct-responses.test.mjs`
- `tests/inference-router-transcript.test.mjs`
- `tests/publication-bootstrap.test.mjs`
- `tests/publication-packaging.test.mjs`
- `tests/reconstructed-updater-guard.test.mjs`
- `tests/research-archives.test.mjs`
- `tests/router-settings.test.mjs`

---

## 01. Overview

> What this reconstruction exposes, who can run it, the hybrid runtime split, and the first docs routes after clone.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/01-overview.md
- Generated: 2026-08-24T05:50:51.653Z

### Source Files

- `README.md`
- `package.json`
- `docs/ARCHITECTURE.md`
- `source/electron-main/main.ts`
- `source/host/main.ts`
- `source/node-agent-coordinator/main.ts`

---
title: "Overview"
description: "What this reconstruction exposes, who can run it, the hybrid runtime split, and the first docs routes after clone."
---

`grok-bot-0.18-reconstructed` (`0.18.0-reconstructed.1`) is an unofficial, source-oriented reconstruction of the public Grok Bot **0.18.0** macOS arm64 app. Readable TypeScript under `source/` is compiled into the packaged Electron main, host, coordinator, and local-exec runtimes. The polished UI stays the checksum-pinned shipped renderer, plus a narrow Settings → Router patch. Output is a separately identified, ad-hoc signed bundle: `dist/Grok Bot 0.18 Reconstructed.app` with bundle id `com.anysphere.sand.reconstructed`. The upstream install on disk is never overwritten.

<Warning>
This is a research reconstruction of a compiled product, not Anysphere's original monorepo and not an official Grok Bot release. No upstream source-code license is implied. Complete an independent rights review before any public redistribution.
</Warning>

## Who can run it

Packaging and launch target **macOS on Apple Silicon** only. `npm run package` aborts on non-`darwin` hosts. The Windows x64 `0.18.0` installer is preserved for research identity; it is not a packaging path.

| Requirement | Constraint |
| --- | --- |
| OS / CPU | macOS, Apple Silicon |
| Node | `>=26.5.0 <27` (26.5.x) |
| Native toolchain | Xcode Command Line Tools |
| Git | Git LFS (`git lfs pull` for the pinned DMG) |
| Docker | Optional; required only for `boxRuntime: "local-docker"` |
| Cursor route | Existing Grok Bot / Cursor session |
| Claude Code route | Local Claude Code install and login |
| Codex route | Local ChatGPT / Codex `auth.json` |
| OpenRouter route | `OPENROUTER_API_KEY` (Settings → Router or env) |

<Note>
`npm run check` (typecheck + `tests/*.test.mjs`) can run without packaging. A working reconstructed app still needs `npm run bootstrap` then `npm run package` on Darwin.
</Note>

## What this reconstruction exposes

Reconstruction plus a small set of control-plane experiments wired through Settings → Router (`window.desktop.agent`):

| Surface | Default | Persist / store |
| --- | --- | --- |
| `SandInferenceProvider` ids `cursor`, `claude-code`, `codex`, `openrouter` | `cursor` | `settings.json` field `inferenceProvider` |
| Local routed usage (`schemaVersion: 1`) | empty per-provider counters | `inferenceRouterUsage` |
| Routed transcript store | `{ schemaVersion: 2, agents: {} }` | `inference-router-transcript.json` |
| `SandBoxRuntime` `remote` \| `local-docker` | `remote` | `boxRuntime` |
| Grok Bot plugin / MCP tools on non-Cursor routes | coordinator intercept | `listRoutedMcpTools` / `executeRoutedMcpTool` |

Cursor keeps the native Grok Bot inference path. `claude-code`, `codex`, and `openrouter` are handled by `createCoordinatorInferenceRouter` in `source/node-agent-coordinator` before the remote gateway. Usage totals in Settings → Usage & Billing are local activity records, not provider invoices.

The Router page also owns **Use local Docker VM**. Enabling `local-docker` starts owned container `grok-bot-local-vm` (loopback gateway `http://127.0.0.1:1340/health`), then restarts the coordinator. Start failure rolls `boxRuntime` back to `remote`.

## Hybrid runtime split

The packaged app is a hybrid by design: compiled reconstruction for control-plane binaries, pinned 0.18.0 renderer for UI, reconstructed identity for the bundle.

```mermaid
flowchart TB
  subgraph ui [Packaged UI]
    Renderer["Checksum-pinned shipped renderer"]
    Patch["Deterministic Settings → Router patch"]
    Frontend["frontend/ Vite workspace — not the packaged UI"]
  end
  subgraph desktop [Desktop process]
    Preload["source/electron-preload"]
    Main["source/electron-main startElectronMain"]
    Settings["settings.json version 1"]
    Secrets["desktop secrets upsert / list"]
  end
  subgraph control [Coordinator and host]
    Coord["source/node-agent-coordinator composeCoordinator"]
    Host["source/host main / production SandHost"]
    Router["createCoordinatorInferenceRouter"]
  end
  subgraph providers [SandInferenceProvider]
    Cursor["cursor — native path"]
    Claude["claude-code"]
    Codex["codex"]
    OpenRouter["openrouter"]
  end
  subgraph box [SandBoxRuntime]
    Remote["remote — default"]
    Docker["local-docker grok-bot-local-vm :1340"]
  end
  Frontend -.->|"GROK_BOT_RENDERER_SOURCE override only"| Renderer
  Renderer --> Patch
  Patch --> Preload
  Preload --> Main
  Main --> Settings
  Main --> Secrets
  Main --> Coord
  Coord --> Host
  Coord --> Router
  Router --> Cursor
  Router --> Claude
  Router --> Codex
  Router --> OpenRouter
  Main --> Remote
  Main --> Docker
  Host --> Remote
  Host --> Docker
```

### Compiled from `source/`

| Tree | Role in the packaged app |
| --- | --- |
| `source/electron-main/` | Window lifecycle, settings/secrets/auth, box connectors, coordinator ownership, RPC (`getInferenceRouter`, `setInferenceRouter`, `getBoxRuntime`, `setBoxRuntime`) |
| `source/electron-preload/` | Trusted bridge to the renderer (`window.desktop.*`) |
| `source/host/` | Turn execution, tools/MCP, settings service, gateway; `main()` is either fail-closed `--box-copy-in` or the long-lived host + gateway |
| `source/node-agent-coordinator/` | Carrier adopt, gateway SSE, transcript/activity, routed MCP bridge, inference router intercept of `sendPrompt` |
| `source/shared/` | `SandInferenceProvider`, `SandBoxRuntime`, `SandSettingsStore` (`SETTINGS_VERSION` 1) |
| `scripts/` | Bootstrap, compile, renderer patch, ASAR replace, ad-hoc sign, verify |

Production binding manifests under `manifests/reconstruction/` must resolve those Electron-main and host ports into `source/`. Bootstrap hydrates ignored `src/app/dist` from the pinned app; that tree is a build input, not authored source.

### Checksum-pinned renderer

The 0.18.0 ASAR did not ship frontend source or source maps. Packaged UI therefore stays the pinned renderer. `scripts/package-macos.mjs` keeps those chunks as the UI authority and installs reconstructed Router UI through a hash-recorded transform (`router` settings tab, provider picker, local Docker toggle, usage rows).

`frontend/` is a Vite design workspace for recovered contracts and clean React. It is **not** the default packaged renderer. `GROK_BOT_RENDERER_SOURCE` can replace `dist/renderer` during ASAR staging; omit it for the patched shipped UI.

### Reconstructed identity

| Identity | Value |
| --- | --- |
| Upstream product | Grok Bot 0.18.0, bundle id `com.anysphere.sand`, Electron 42.1.0 |
| Reconstructed display name | `Grok Bot 0.18 Reconstructed` |
| Reconstructed bundle id | `com.anysphere.sand.reconstructed` |
| Output path | `dist/Grok Bot 0.18 Reconstructed.app` (`GROK_BOT_OUTPUT_APP_NAME` overrides the basename) |
| Signature | Ad-hoc only; upstream Developer ID / notarization is not reused |
| Updater / telemetry defaults at pack | `SAND_DISABLE_UPDATES`, `SAND_DISABLE_SENTRY`, `SAND_DISABLE_TELEMETRY` default to `"1"` if unset |

Pinned artifact identities:

| Artifact | SHA-256 |
| --- | --- |
| `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| Original `app.asar` | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |
| Windows x64 `Grok_Bot_0.18.0_Setup.exe` (LFS preserve only) | `464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e` |

Evidence-only UI rule: do not invent screens, labels, or interactions to fill an evidence gap. Incomplete renderer mapping stays unmapped.

## First path after clone

<Steps>
<Step title="Install prerequisites">
Node 26.5.x, Xcode Command Line Tools, Git LFS. Docker only if you will toggle local-docker.
</Step>
<Step title="Clone and hydrate LFS">

```sh
git clone <your-repository-url>
cd grok-bot-0.18-reconstructed
git lfs install
git lfs pull
npm ci
```

</Step>
<Step title="Bootstrap the pinned 0.18.0 runtime">

```sh
npm run bootstrap
```

Resolution order: `GROK_BOT_018_APP` → cached `.cache/runtime/Grok Bot.app` → Git LFS DMG → public URL `https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg`. Bootstrap verifies DMG and `app.asar` SHA-256 and hydrates ignored `src/app/dist`.

Expected log fragment:

```text
Runtime ready: .../.cache/runtime/Grok Bot.app
Checksum-pinned source payload ready: .../src/app/dist (...)
```

</Step>
<Step title="Check, package, open">

```sh
npm run check
npm run package
open "dist/Grok Bot 0.18 Reconstructed.app"
```

`package` compiles runtimes, patches Router settings, replaces `app.asar`, sets `com.anysphere.sand.reconstructed`, ad-hoc signs, and verifies. Success includes:

```text
Packaged application: .../dist/Grok Bot 0.18 Reconstructed.app (...)
```

Optional: `npm run verify` on an existing bundle; `npm run smoke` native e2e with a 12s timeout (`Smoke verification: PASS`).
</Step>
</Steps>

:::files
repository
├── source/                  compiled Electron / host / coordinator / shared
├── frontend/                Vite design workspace (not default packaged UI)
├── scripts/                 bootstrap, compile, patch, package, verify
├── manifests/reconstruction/ production bindings that must resolve into source/
├── research-archives/original/0.18.0/  Git LFS 0.18.0 DMG + Windows setup
├── src/app/dist/            ignored; hydrated by bootstrap
├── dist/                    ignored; packaged reconstructed .app
└── tests/                   publication, router, bootstrap, smoke regressions
:::

Ignored generated trees also include `.cache`, `.build`, `recovered`, and `recovery`. Do not commit them.

## Commands that define the product loop

| Script | What it does |
| --- | --- |
| `npm run bootstrap` | Pin and hydrate the 0.18.0 Electron shell + `src/app/dist` |
| `npm run check` | `typecheck` + `source:typecheck` + `npm test` |
| `npm run package` | `check` then Darwin package / sign / verify |
| `npm run verify` | Required ASAR paths and evidence markers |
| `npm run smoke` | Native e2e against `dist/Grok Bot 0.18 Reconstructed.app` |
| `npm run publication:check` | Clean-export tree equality before a public remote |
| `npm run frontend:build` | Vite build of the design workspace |

## Next

<CardGroup>
<Card title="Installation" href="/installation">
macOS Apple Silicon prerequisites, Node 26.5.x, Git LFS, Xcode tools, optional Docker, and npm ci.
</Card>
<Card title="Quickstart" href="/quickstart">
Clone, LFS pull, bootstrap, check, package, and open the reconstructed app with the expected success signal.
</Card>
<Card title="Reconstruction boundary" href="/reconstruction-boundary">
Compiled `source/` runtimes, checksum-pinned shipped renderer, bundle id `com.anysphere.sand.reconstructed`, ignored `src/app/dist`.
</Card>
<Card title="Inference router" href="/inference-router">
Provider ids, default `cursor`, transcript `schemaVersion` 2, usage `schemaVersion` 1.
</Card>
<Card title="Box runtime" href="/box-runtime">
`remote` vs `local-docker`, coordinator restart on switch, loopback-only local gateway.
</Card>
<Card title="Evidence and provenance" href="/evidence-and-provenance">
Pinned DMG and `app.asar` SHA-256 identities, evidence-only UI rule, production bindings.
</Card>
</CardGroup>

---

## 02. Installation

> macOS Apple Silicon prerequisites, Node 26.5.x, Git LFS, Xcode tools, optional Docker, and npm ci.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/02-installation.md
- Generated: 2026-08-24T05:53:49.074Z

### Source Files

- `package.json`
- `README.md`
- `scripts/lib/system-tools.mjs`
- `scripts/lib/config.mjs`
- `research-archives/README.md`
- `scripts/apply-third-party-patches.mjs`

---
title: "Installation"
description: "macOS Apple Silicon prerequisites, Node 26.5.x, Git LFS, Xcode tools, optional Docker, and npm ci."
---

The reconstruction is a Node workspace that installs with `npm ci` from `package-lock.json` (`lockfileVersion` 3) and then hydrates a checksum-pinned 0.18.0 macOS arm64 Electron shell. `package.json` names the package `grok-bot-0.18-reconstructed` at `0.18.0-reconstructed.1`, sets `"type": "module"`, and constrains `engines.node` to `>=26.5.0 <27`. `npm ci` also runs `postinstall` (`node scripts/apply-third-party-patches.mjs`). Packaging (`scripts/package-macos.mjs`) throws if `process.platform !== "darwin"`. The pinned DMG is the Git LFS object `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` (SHA-256 `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb`).

<Warning>
Do not treat this checkout as a cross-platform app factory. The Windows x64 installer is preserved for research identity only. `npm run package` and `npm run smoke` require Darwin; the official shell is a thin arm64 Mach-O (`0xfeedfacf`).
</Warning>

## Supported host

| Constraint | Value | Enforced by |
| --- | --- | --- |
| OS | macOS (`darwin`) | `scripts/package-macos.mjs`, `scripts/package-fidelity-diagnostic.mjs` |
| CPU | Apple Silicon (`arm64`) | Pinned DMG `darwin-arm64/0.18.0`; Mach-O parser in `scripts/lib/macos-shell-invariant.mjs` |
| Node | `>=26.5.0 <27` | `package.json` `engines.node` |
| Package manager | `npm ci` against the committed lockfile | `package-lock.json`, `CONTRIBUTING.md` |
| Bundle identity after package | `com.anysphere.sand.reconstructed` | `scripts/lib/config.mjs` `reconstructedBundleId` |
| Default output app | `dist/Grok Bot 0.18 Reconstructed.app` | `GROK_BOT_OUTPUT_APP_NAME` override, else `reconstructedName` |

`src/app/package.json` still reports upstream product `sand` / `Grok Bot` `0.18.0`. That file is the hydrated app manifest, not the reconstruction workspace identity.

## Prerequisites

| Tool | Required for | Notes |
| --- | --- | --- |
| Node.js 26.5.x (any `26.5.0`–`<27`) | `npm ci`, tests, TypeScript, scripts | README documents 26.5.x; `engines` is the hard range |
| Git LFS | `research-archives/original/**/*.dmg` and `*.exe` | `.gitattributes` `filter=lfs` |
| Xcode Command Line Tools | bootstrap attach, ad-hoc sign, `plutil`, `node-gyp` | Absolute tool paths in `scripts/lib/system-tools.mjs` |
| Docker CLI + a running daemon | optional local box (`local-docker`) | Default box runtime stays `remote` |
| Cursor / Claude Code / Codex / OpenRouter auth | routed inference after launch | Not part of `npm ci` |

<Note>
Provider sessions are runtime credentials, not install-time packages. Cursor is the default inference provider. Claude Code and Codex reuse existing local logins; OpenRouter needs `OPENROUTER_API_KEY` later.
</Note>

## Install Node 26.5.x

Confirm the host interpreter before `npm ci`:

```sh
node -v
# expected: v26.5.x  (engines: >=26.5.0 <27)
```

`@types/node` is pinned at `26.2.0`. Electron `42.1.0` is a `devDependency` (headers/ABI 146, Electron Node `24.15.0` for native rebuilds). Host Node 26 and the Electron runtime Node 24 are different processes; do not swap the workspace interpreter to 24 to match Electron.

## Xcode Command Line Tools

Bootstrap, package, and verify spawn Apple binaries by absolute path (`shell: false` in `scripts/lib/process.mjs`). Those binaries come from Xcode Command Line Tools:

| Constant | Path | Used for |
| --- | --- | --- |
| `hdiutil` | `/usr/bin/hdiutil` | Attach/detach the 0.18.0 DMG during `npm run bootstrap` |
| `ditto` | `/usr/bin/ditto` | Copy `Grok Bot.app` into `.cache/runtime/` and `dist/` |
| `plutil` | `/usr/bin/plutil` | Read/write `Info.plist` (`CFBundleIdentifier`, URL types, version) |
| `codesign` | `/usr/bin/codesign` | Ad-hoc sign (`--sign -`) and `--verify --deep --strict` |
| `xattr` | `/usr/bin/xattr` | `xattr -cr` on the reconstructed `.app` (clear quarantine) |
| `cp` / `lsof` / `ps` | `/bin/cp`, `/usr/sbin/lsof`, `/bin/ps` | Copy and native smoke/process inspection |

`node-gyp@12.1.0` and `node-addon-api@8.5.0` are workspace `devDependencies`. `postinstall` rewrites `node_modules/tree-sitter/binding.gyp` so Node ≥ 22 uses C++20 (`cxxstd`). Packaging stages tree-sitter via `scripts/build-tree-sitter-node.mjs`. Missing CLT shows up as `hdiutil`/`codesign`/`node-gyp` spawn failures, not as a custom Node error.

If `/usr/bin/hdiutil` or `/usr/bin/codesign` is missing:

```sh
xcode-select --install
xcode-select -p
```

## Git LFS

`.gitattributes` tracks:

```text
research-archives/original/**/*.dmg filter=lfs diff=lfs merge=lfs -text
research-archives/original/**/*.exe filter=lfs diff=lfs merge=lfs -text
```

`artifacts.json` (`schemaVersion` 1) is the size/digest inventory. A pointer-sized file fails `tests/research-archives.test.mjs` with `requires git lfs pull` because `lstat` size must equal `artifact.bytes`.

| Artifact | Bytes | SHA-256 |
| --- | ---: | --- |
| `macos-arm64/Grok_Bot_0.18.0.dmg` | 155793020 | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| `windows-x64/Grok_Bot_0.18.0_Setup.exe` | 125825552 | `464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e` |

If the archived DMG is present but the digest is wrong, `scripts/bootstrap-runtime.mjs` throws `Archived DMG checksum mismatch … Run git lfs pull before bootstrapping.`

## npm ci

`npm ci` is the supported install. It materializes `node_modules/` from `package-lock.json` and sets `hasInstallScript: true`.

`postinstall` / `npm run native:patch` patches only stock SHA-256 inputs:

| Target | Purpose |
| --- | --- |
| `@connectrpc/connect` ESM + CJS `protocol-connect/transport.js` | Set `Content-Length: 0` on empty request bodies |
| `tree-sitter/binding.gyp` | Select C++17 vs C++20 from `npm_config_target` / `process.versions.node` |

Unexpected bytes abort: `Refusing to patch unexpected @connectrpc/connect input …`. Already-patched files are skipped (`patchedSha256`).

Generated trees stay out of Git (`.gitignore`): `node_modules/`, `.cache/`, `.build/`, `dist/`, `src/app/dist/`, `recovered/`.

:::files
repo/
  package.json                 # engines.node >=26.5.0 <27; postinstall
  package-lock.json            # lockfileVersion 3; npm ci input
  scripts/apply-third-party-patches.mjs
  scripts/lib/system-tools.mjs
  scripts/lib/config.mjs
  research-archives/original/0.18.0/
    artifacts.json
    SHA256SUMS
    macos-arm64/Grok_Bot_0.18.0.dmg   # Git LFS
    windows-x64/Grok_Bot_0.18.0_Setup.exe
  src/app/                     # package.json checked in; dist/ ignored until bootstrap
  node_modules/                # created by npm ci
  .cache/runtime/Grok Bot.app  # created by bootstrap
  dist/                        # created by package
:::

## Optional Docker

Local sandbox is not an install gate. `source/electron-main/box/local-docker-host-connector.ts` shells out to `docker` and probes `docker info`. Constants:

| Name | Value |
| --- | --- |
| `LOCAL_DOCKER_BOX_IMAGE` | `public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest` |
| `LOCAL_DOCKER_BOX_CONTAINER` | `grok-bot-local-vm` |
| `LOCAL_DOCKER_GATEWAY_URL` | `http://127.0.0.1:1340` |
| `LOCAL_DOCKER_OWNER_LABEL` | `com.grok-bot.local-vm=1` |
| `LOCAL_DOCKER_SCHEMA_VERSION` | `6` |

If the daemon is down, status is `available: false` with detail `Docker is not running.` or the `docker` spawn error. Default box mode remains `remote`.

## Install the workspace

<Steps>
<Step title="Clone and enable LFS">

```sh
git clone <your-repository-url>
cd grok-bot-0.18-reconstructed
git lfs install
git lfs pull
```

</Step>
<Step title="Confirm Node and Apple tools">

```sh
node -v
uname -m
test -x /usr/bin/hdiutil && test -x /usr/bin/codesign && echo ok
```

`uname -m` must be `arm64`. `node -v` must satisfy `>=26.5.0 <27`.

</Step>
<Step title="Install lockfile dependencies">

```sh
npm ci
```

Expect `postinstall` to finish without patch-drift errors. Do not use a floating `npm install` as the documented contributor path.

</Step>
<Step title="Verify LFS payloads">

```sh
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
```

Both lines must print `OK`. Optional: `git lfs ls-files` should list both installers.

</Step>
</Steps>

<ParamField body="GROK_BOT_018_APP" type="string">
Optional absolute path to an already-extracted `Grok Bot.app`. When set, bootstrap copies that app with `ditto` instead of attaching the DMG. `CFBundleShortVersionString` must be `0.18.0`.
</ParamField>

<ParamField body="GROK_BOT_OUTPUT_APP_NAME" type="string">
Optional basename for the packaged app under `dist/`. Default is `Grok Bot 0.18 Reconstructed.app`.
</ParamField>

Installation stops at a complete `node_modules/` plus real LFS binaries. Hydrating `src/app/dist` is `npm run bootstrap`. Compiling runtimes and writing the `.app` is `npm run package`.

## Verification

<Check>
A successful install has Node in range, `npm ci` exit 0, Connect/tree-sitter patches applied, and both research-archive checksums matching `SHA256SUMS`.
</Check>

```sh
node -e "const e=require('./package.json').engines.node; console.log(process.version, e)"
npm ci
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
```

`npm run check` (`typecheck` + `source:typecheck` + `node --test tests/*.test.mjs`) can run after `npm ci` without packaging. The archives test still requires pulled LFS objects.

Contributor minimum after install (from `CONTRIBUTING.md`):

```sh
npm ci
npm run check
npm run frontend:build
```

On macOS, after bootstrap:

```sh
npm run package
npm run verify
```

## Install failures

| Symptom | Cause | Fix |
| --- | --- | --- |
| `engines` / unexpected Node | Interpreter outside `>=26.5.0 <27` | Install Node 26.5.x |
| DMG tens of kilobytes; test `requires git lfs pull` | LFS pointer, not the 155 793 020-byte object | `git lfs install && git lfs pull` |
| `Archived DMG checksum mismatch` | Corrupt or pointer archive | `git lfs pull`, then `shasum -c SHA256SUMS` |
| `Refusing to patch unexpected @connectrpc/connect input` | `node_modules` not lockfile-stock | Delete `node_modules` and re-run `npm ci` |
| `The reconstructed macOS application can only be packaged on macOS.` | `process.platform !== "darwin"` | Use Apple Silicon macOS for package/smoke |
| `Expected Grok Bot 0.18.0, got …` | `GROK_BOT_018_APP` is the wrong app version | Point at 0.18.0 or unset and use the LFS DMG |
| `hdiutil` / `codesign` exited non-zero | Missing CLT or Gatekeeper tool path | `xcode-select --install` |
| `Docker is not running.` | Optional daemon down | Ignore unless enabling local Docker VM |

<Warning>
Do not weaken checksum, bundle-id, or signing checks to get a green install. Those gates are required contributor policy.
</Warning>

## Next

<CardGroup>
<Card title="Quickstart" href="/quickstart">
Clone through `npm run package` and open `dist/Grok Bot 0.18 Reconstructed.app`.
</Card>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
`GROK_BOT_018_APP`, cached app, LFS DMG, then public URL; hydrate `src/app/dist`.
</Card>
<Card title="Preserve original installers" href="/preserve-original-installers">
LFS identities, `artifacts.json` schemaVersion 1, and `shasum -c SHA256SUMS`.
</Card>
<Card title="Enable the local Docker sandbox" href="/enable-local-docker">
Optional `grok-bot-local-vm` after Docker is installed and running.
</Card>
<Card title="npm scripts" href="/npm-scripts">
Exact `package.json` commands after `npm ci`.
</Card>
<Card title="Bootstrap failures" href="/bootstrap-failures">
Pointer-sized archives, checksum mismatch, and hydration errors.
</Card>
</CardGroup>

---

## 03. Quickstart

> Clone, LFS pull, bootstrap, check, package, and open dist/Grok Bot 0.18 Reconstructed.app with the expected success signal.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/03-quickstart.md
- Generated: 2026-08-24T05:51:26.446Z

### Source Files

- `README.md`
- `package.json`
- `scripts/bootstrap-runtime.mjs`
- `scripts/package-macos.mjs`
- `scripts/verify.mjs`
- `scripts/lib/config.mjs`

---
title: "Quickstart"
description: "Clone, LFS pull, bootstrap, check, package, and open dist/Grok Bot 0.18 Reconstructed.app with the expected success signal."
---

`npm run bootstrap` then `npm run package` is the supported path from a clone to a launchable macOS app. Bootstrap resolves a checksum-pinned Grok Bot **0.18.0** runtime, hydrates ignored `src/app/dist`, and caches `.cache/runtime/Grok Bot.app`. Package compiles reviewed `source/` runtimes, copies that Electron shell, replaces `app.asar`, sets bundle id `com.anysphere.sand.reconstructed`, ad-hoc signs, and writes `dist/Grok Bot 0.18 Reconstructed.app`. The installed upstream Grok Bot.app is never overwritten.

<Warning>
Packaging aborts unless `process.platform === "darwin"`. The pinned installer is macOS arm64. Node must satisfy `>=26.5.0 <27`.
</Warning>

## Prerequisites

| Requirement | Constraint |
| --- | --- |
| Host | macOS on Apple Silicon |
| Node | `package.json` `engines.node`: `>=26.5.0 <27` |
| Git LFS | Required for `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` |
| Xcode CLT | Native compile during package (`node-gyp`, tree-sitter, codesign, `hdiutil`, `ditto`, `plutil`, `xattr`) |
| Docker | Optional. First launch defaults `boxRuntime` to `remote` |

Full host setup lives on [Installation](/installation). Docker is only required if you later enable the local sandbox.

## Clone and LFS

<Steps>
<Step title="Clone the reconstruction">

```sh
git clone https://github.com/sashimikun/grok-bot-0.18-reconstructed.git
cd grok-bot-0.18-reconstructed
```

</Step>
<Step title="Materialize Git LFS installers">

```sh
git lfs install
git lfs pull
```

The macOS pin is `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` at **155,793,020** bytes, SHA-256 `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb`.

<RequestExample>
```sh
cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS
```
</RequestExample>

If `lstat` size is not the pinned byte count, the file is still an LFS pointer. Bootstrap then fails with `Archived DMG checksum mismatch: expected a253ccd8… Run git lfs pull before bootstrapping.`

</Step>
<Step title="Install lockfile dependencies">

```sh
npm ci
```

`postinstall` applies third-party patches. Do not commit `.cache/`, `.build/`, `dist/`, or `src/app/dist/`.

</Step>
</Steps>

## Bootstrap the pinned runtime

```sh
npm run bootstrap
```

This is `node scripts/bootstrap-runtime.mjs`. Resolution order:

```mermaid
flowchart TD
  subgraph operator [Operator and archive inputs]
    env["GROK_BOT_018_APP"]
    lfsDmg["research-archives/.../Grok_Bot_0.18.0.dmg"]
    publicUrl["downloads.cursor.com/.../0.18.0/Grok_Bot_0.18.0.dmg"]
  end
  subgraph cache [Local cache]
    cachedApp[".cache/runtime/Grok Bot.app"]
    cachedDmg[".cache/downloads/Grok_Bot_0.18.0.dmg"]
  end
  subgraph hydrate [Checksum-pinned payload]
    asar["Contents/Resources/app.asar"]
    dest["src/app/dist"]
  end
  env -->|version must be 0.18.0| cachedApp
  cachedApp -->|if already valid| asar
  lfsDmg -->|SHA-256 a253ccd8…| cachedDmg
  publicUrl -->|fallback fetch| cachedDmg
  cachedDmg -->|hdiutil attach Grok Bot.app| cachedApp
  asar -->|SHA-256 66654081…| dest
```

| Priority | Source | Gate |
| --- | --- | --- |
| 1 | `GROK_BOT_018_APP` | `CFBundleShortVersionString` must be `0.18.0`; `Contents/MacOS/Grok Bot` and `app.asar.unpacked` must exist |
| 2 | `.cache/runtime/Grok Bot.app` | Same version/completeness check |
| 3 | LFS DMG → `.cache/downloads/Grok_Bot_0.18.0.dmg` | SHA-256 `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| 4 | Public URL `dmgUrl` | Same DMG SHA-256 after download |

Hydration extracts `app.asar` only when its SHA-256 is `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58`, and requires `dist/electron-main/main.cjs`, `dist/host/host-main.cjs`, and `dist/renderer/index.html`.

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute path to an existing 0.18.0 `Grok Bot.app`. Skips DMG attach. Still hydrates `src/app/dist` from that app’s `app.asar`.
</ParamField>

<ResponseExample>
```text
Using archived release …/research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg
Runtime ready: …/.cache/runtime/Grok Bot.app
Checksum-pinned source payload ready: …/src/app/dist (6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58)
The checksum-pinned app supplies only the Electron shell, ABI-matched native dependencies, and explicitly documented build fallbacks.
```
</ResponseExample>

A later `npm run bootstrap` reuses `.cache/runtime/Grok Bot.app` when it still validates as 0.18.0.

## Check, package, and open

`npm run package` already runs `npm run check` (`typecheck` + `source:typecheck` + `node --test tests/*.test.mjs`). You can run check alone to fail faster.

```sh
npm run check
npm run package
open "dist/Grok Bot 0.18 Reconstructed.app"
```

`npm run package` is `npm run check && node scripts/package-macos.mjs`. That script:

1. Builds the reconstructed ASAR from `source/` plus the checksum-pinned renderer baseline.
2. Copies `.cache/runtime/Grok Bot.app` into `dist/` with `ditto`.
3. Clears quarantine with `xattr -cr` so Gatekeeper does not reject the ad-hoc signature.
4. Replaces `Contents/Resources/app.asar` and `app.asar.unpacked`.
5. Sets `CFBundleIdentifier` to `com.anysphere.sand.reconstructed` and `CFBundleDisplayName` to `Grok Bot 0.18 Reconstructed`.
6. Registers URL scheme `sand` only (inherited aliases such as `grokbot` are removed).
7. Ad-hoc signs, retries once on nested-framework signature races, then `codesign --verify --deep --strict`.

<ParamField body="GROK_BOT_OUTPUT_APP_NAME" type="string">
Optional basename under `dist/`. Default output is `dist/Grok Bot 0.18 Reconstructed.app`.
</ParamField>

<ResponseExample>
```text
Packaged application: …/dist/Grok Bot 0.18 Reconstructed.app (N native manifest entries, M unpacked runtime files)
```
</ResponseExample>

That `Packaged application:` line is the packaging success signal. `open` then launches the reconstructed bundle, not the original `/Applications` install.

:::files
repo/
  research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg   # Git LFS pin
  .cache/downloads/Grok_Bot_0.18.0.dmg                                # verified DMG cache
  .cache/runtime/Grok Bot.app                                         # ABI-matched Electron shell
  src/app/dist/                                                       # hydrated, gitignored
  dist/Grok Bot 0.18 Reconstructed.app                                # launchable output
    Contents/Info.plist                                               # com.anysphere.sand.reconstructed
    Contents/Resources/app.asar                                       # reconstructed payload
:::

## Optional verify and smoke

These are not required to open the app. `package` already runs an internal reconstructed-package audit.

| Command | What it proves | Success signal |
| --- | --- | --- |
| `npm run verify` | Required ASAR entries, evidence markers (≥1,000 `// src/` lines in hydrated mains), renderer provenance, bundle id, `sand` URL scheme, codesign | `Verified packaged ASAR …` then `Verified N executable clean-source runtimes…` |
| `npm run smoke` | Native e2e against `outputApp`, 12s timeout, isolated user-data dirs, `SAND_DISABLE_UPDATES/TELEMETRY/ANALYTICS=1` | `Smoke verification: PASS` (exit 0). Non-darwin is `prerequisite` (exit 2) |

```sh
npm run verify
npm run smoke
```

`npm run verify -- --app /absolute/path/to/App.app` retargets the audit. Packaged electron-main injects `SAND_DISABLE_UPDATES ??= "1"`, `SAND_DISABLE_SENTRY ??= "1"`, and `SAND_DISABLE_TELEMETRY ??= "1"`; explicit env still wins.

## First launch defaults

The reconstructed app is a hybrid: runtimes from `source/`, UI from the checksum-pinned shipped renderer plus a narrow Settings → Router patch. `frontend/` is a Vite design workspace and is **not** the packaged renderer unless you override that on a later build.

| Surface | Default | Notes |
| --- | --- | --- |
| Inference | `inferenceProvider` `cursor` | Other ids: `claude-code`, `codex`, `openrouter`. Cursor uses the existing Grok Bot/Cursor session |
| Box | `boxRuntime` `remote` | Local Docker is opt-in; leave it off for first launch |
| Identity | `com.anysphere.sand.reconstructed` | Ad-hoc signed; CFBundleName/executable stay `Grok Bot` for helper ABI match |
| Deep link | scheme `sand` | Auth callback target for the reconstructed bundle |

Open **Settings → Router** after launch. Cursor needs no extra key. Claude Code, Codex, and OpenRouter require their own local login or `OPENROUTER_API_KEY` before those providers work.

## Command map

| Script | Implementation | Role in this path |
| --- | --- | --- |
| `npm run bootstrap` | `scripts/bootstrap-runtime.mjs` | Pin, extract, hydrate |
| `npm run check` | frontend + `source/` `tsc`, then `tests/*.test.mjs` | Required gate |
| `npm run package` | `check` then `scripts/package-macos.mjs` | Produce the `.app` |
| `npm run verify` | `scripts/verify.mjs` | Re-audit an existing bundle |
| `npm run smoke` | `scripts/smoke.mjs` | Bounded native launch |

## Quick failure map

| Symptom | Likely cause |
| --- | --- |
| `Archived DMG checksum mismatch` | LFS pointer, not the 155,793,020-byte DMG |
| `DMG checksum mismatch` after download | Public file does not match `dmgSha256` |
| `Expected Grok Bot 0.18.0, got …` | `GROK_BOT_018_APP` or cache is the wrong version |
| `Upstream app.asar checksum mismatch` | Hydration pin `66654081…` failed |
| `Upstream app.asar is missing dist/…` | Incomplete asar |
| `The reconstructed macOS application can only be packaged on macOS.` | Non-darwin `npm run package` |
| Gatekeeper reject of a copied app | Quarantine xattrs; package already runs `xattr -cr` on `outputApp` |

## Next

<CardGroup>
<Card title="Installation" href="/installation">
macOS Apple Silicon, Node 26.5.x, Git LFS, Xcode tools, optional Docker, and `npm ci`.
</Card>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
Full `GROK_BOT_018_APP` / cache / LFS / URL order and SHA-256 gates.
</Card>
<Card title="Package the macOS app" href="/package-macos-app">
Renderer patch, ASAR replace, reconstructed identity, ad-hoc sign, verify.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
Required ASAR paths, evidence markers, 12s native e2e, updater guards.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Settings → Router persistence and Cursor / Claude Code / Codex / OpenRouter auth.
</Card>
<Card title="Bootstrap failures" href="/bootstrap-failures">
Pointer-sized archives, DMG/asar mismatches, version and hydration errors.
</Card>
</CardGroup>

---

## 04. Reconstruction boundary

> Compiled source/ runtimes, checksum-pinned shipped renderer, reconstructed bundle id com.anysphere.sand.reconstructed, and ignored src/app/dist hydration.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/04-reconstruction-boundary.md
- Generated: 2026-08-24T05:51:53.325Z

### Source Files

- `docs/ARCHITECTURE.md`
- `PROVENANCE.md`
- `scripts/lib/config.mjs`
- `scripts/package-macos.mjs`
- `scripts/lib/build-asar.mjs`
- `src/app/package.json`

---
title: "Reconstruction boundary"
description: "Compiled source/ runtimes, checksum-pinned shipped renderer, reconstructed bundle id com.anysphere.sand.reconstructed, and ignored src/app/dist hydration."
---

`npm run package` assembles a **fidelity hybrid** macOS app: reviewed TypeScript under `source/` is compiled into ASAR executables, the UI stays the checksum-pinned 0.18.0 renderer from ignored `src/app/dist/renderer` plus a recorded Settings Router patch, and the finished bundle is identified as `com.anysphere.sand.reconstructed` with an ad-hoc signature. The official 0.18.0 app is a reference-only, SHA-256-pinned input; packaging copies its Electron shell and ABI-matched natives, then replaces `app.asar` so the reconstructed package never ships the official archive or Developer ID signature.

<Warning>
Do not treat reconstructed material as original source or an official Grok Bot build. Upstream bundle ID is `com.anysphere.sand`. Reconstructed builds use `com.anysphere.sand.reconstructed`, ad-hoc signing only, and must not overwrite a machine-installed official app.
</Warning>

## Ownership map

```mermaid
flowchart TB
  subgraph tracked["Git-tracked reconstruction"]
    SRC["source/ Electron main, host, coordinator, daemons, preloads"]
    FE["frontend/ Vite design workspace"]
    PKG["src/app/package.json name sand version 0.18.0"]
    SCRIPTS["scripts/ bootstrap, compile, patch, package, verify"]
  end

  subgraph ignored["Ignored hydration"]
    DIST["src/app/dist from app.asar SHA-256 66654081…"]
    CACHE[".cache/runtime/Grok Bot.app"]
  end

  subgraph packaged["dist/Grok Bot 0.18 Reconstructed.app"]
    SHELL["Electron 42.1.0 shell CFBundleName Grok Bot"]
    ASAR["replaced app.asar hybrid runtimes"]
    ID["CFBundleIdentifier com.anysphere.sand.reconstructed"]
  end

  DMG["Pinned 0.18.0 DMG a253ccd8…"] --> CACHE
  CACHE --> DIST
  DIST --> ASAR
  SRC --> ASAR
  FE -.->|"not the packaged UI unless GROK_BOT_RENDERER_SOURCE"| ASAR
  PKG --> ASAR
  SCRIPTS --> packaged
  SHELL --> ID
  ASAR --> ID
```

Tracked `src/app/package.json` is the app metadata seed (`name`: `sand`, `productName`: `Grok Bot`, `version`: `0.18.0`, `main`: `dist/electron-main/main.cjs`). Generated `src/app/dist/` is gitignored and must be hydrated before compile or package.

## Hybrid runtime split

Default packaging calls `buildFidelityReconstructedAsar()`. Build kind in `dist/reconstruction-build.json` is `fidelity-hybrid-reconstruction`. Renderer mode is `checksum-pinned-artifact-runtime` with artifact root `src/app/dist/renderer`.

| Runtime | Packaged path | Default mode | Source of bytes |
| --- | --- | --- | --- |
| `renderer` | `dist/renderer` | `checksum-pinned-artifact-runtime` | Hydrated shipped UI; Settings Router patch only |
| `electron-main` | `dist/electron-main/main.cjs` | `clean-source` when `manifests/reconstruction/electron-main-production-bindings-manifest.json` validates; else `artifact-fallback` | `source/electron-main/main.ts` or hydrated `main.cjs` plus updater/dev seams |
| `host` | `dist/host/host-main.cjs` | `artifact-fallback` unless `GROK_BOT_HOST_BINDINGS_MANIFEST` binds every required slot into `source/` | `source/host/main.ts` or hydrated `host-main.cjs` |
| `node-agent-coordinator` | `dist/node-agent-coordinator/main.cjs` | `clean-source` | `source/node-agent-coordinator/main.ts` |
| `primary-preload` | `dist/electron-preload/preload.cjs` | `clean-source` | `source/electron-preload/runtime/primary.ts` |
| `dev-controls-preload` / `webview-preload` / `vnc-preload` | matching `preload-*.cjs` | `clean-source` | `source/electron-preload/runtime/` |
| `electron-dev-controls` | `dist/electron-dev-controls/main.cjs` | `clean-source` | `source/electron-dev-controls/main.ts` |
| Host workers | `agent-store-worker.cjs`, `transcript-mirror-worker.cjs`, `box-store-vacuum-worker.cjs`, `search-index-worker.cjs` | `clean-source` | matching `source/host/**` entries |
| `box-exec-daemon` | `dist/box-exec-daemon/main.cjs` | `clean-source` | `source/box-exec-daemon/cli.ts` |
| `local-exec-daemon` | `dist/local-exec-daemon/main.cjs` | `clean-source` | `source/local-exec-daemon/main.ts` |
| `electron-runtime-dependencies` | `dist/deps` | `artifact-runtime` | Unpacked 0.18.0 `app.asar.unpacked/dist/deps` |
| `native-runtime-tools` | `dist/native` | `artifact-runtime` | Unpacked 0.18.0 `dist/native` |
| `node-runtime-dependencies` | `dist/node-deps` | `generated-runtime` | Local Node ABI tree-sitter rebuild |
| `electron-shell` | `Contents/Frameworks/Electron Framework.framework` | `artifact-runtime` | Pinned 0.18.0 `.app` via `ditto` |

Clean overlays replace coordinator, daemons, preloads, workers, and `dist/node-deps`. Fidelity packaging **does not** overlay `dist/renderer` from `frontend/`. Artifact-fallback copies of recovered source land at `dist/recovered-source/electron-main/main.cjs` and `dist/recovered-source/host/host-main.cjs` only while those runtimes stay in fallback mode; a clean activation **removes** that recovered-source entry.

A separate source-aware path (`buildReconstructedAsar`) can classify the renderer as `clean-source` from `frontend/src/main.tsx`. `npm run package` does not use that path; `tests/publication-packaging.test.mjs` asserts the default packager imports `buildFidelityReconstructedAsar`.

## Ignored `src/app/dist` hydration

`npm run bootstrap` (`scripts/bootstrap-runtime.mjs`) is the only supported way to populate the compile input.

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute path to an existing Grok Bot 0.18.0 `.app`. Takes precedence over cache and DMG.
</ParamField>

Resolution order:

1. `GROK_BOT_018_APP` → cache with `ditto` to `.cache/runtime/Grok Bot.app`
2. Existing `.cache/runtime/Grok Bot.app` if `CFBundleShortVersionString` is `0.18.0`
3. Git LFS DMG `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg`
4. Public URL `https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg`

Required identities:

| Input | Expected SHA-256 |
| --- | --- |
| DMG | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| `Contents/Resources/app.asar` | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |

Hydration extracts the ASAR into a temp dir, requires these files, then replaces **only** `src/app/dist`:

- `dist/electron-main/main.cjs`
- `dist/host/host-main.cjs`
- `dist/renderer/index.html`

`.gitignore` lists `/src/app/dist/`. Do not commit hydrated payloads, `.cache/`, `.build/`, or `dist/`.

<RequestExample>
```sh title="Hydrate the pinned 0.18.0 payload"
git lfs pull
npm run bootstrap
```
</RequestExample>

<ResponseExample>
```text title="Expected bootstrap signal"
Runtime ready: …/.cache/runtime/Grok Bot.app
Checksum-pinned source payload ready: …/src/app/dist (<asar sha256>)
The checksum-pinned app supplies only the Electron shell, ABI-matched native dependencies, and explicitly documented build fallbacks.
```
</ResponseExample>

## Checksum-pinned renderer

The 0.18.0 ASAR shipped optimized production chunks, not authored frontend source or source maps. Packaged UI authority is that inventory, recorded as `dist/renderer-artifact-provenance.json` (`schemaVersion` 1, `mode` `checksum-pinned-artifact-runtime`, `upstreamAppAsarSha256` equal to the pinned ASAR digest, per-file SHA-256 list).

The only default UI transform is `applyOriginalRendererRouterPatch`:

- Finds exactly one Settings registry chunk and one panel chunk under staged `dist/renderer/assets/*.js`
- Inserts Settings tab `router` / label `Router`
- Records `dist/renderer-router-extension.json` (`mode` `original-renderer-settings-extension`, features `settings-router-provider`, `settings-local-docker-vm`, `usage-current-provider`)
- Stores original and patched byte counts and SHA-256 for those chunks

Evidence-only rule: recovered UI may express only behavior with an inspectable artifact anchor. Do not invent screens, routes, controls, or labels to fill gaps. `frontend/` is a readable Vite workspace and typecheck/build target; it is not the packaged renderer.

<ParamField body="GROK_BOT_RENDERER_SOURCE" type="string">
Optional path (resolved from the repo root) whose `index.html` replaces staged `dist/renderer` inside `buildAsar` **before** the fidelity overlay. Overlay still leaves the renderer as artifact-runtime, then the Settings patch runs. The override must still contain the original registry and panel anchors or packaging throws `Expected one original Settings registry and panel chunk`.
</ParamField>

## Reconstructed macOS identity

`scripts/package-macos.mjs` aborts unless `process.platform === "darwin"`. It `ditto`s the cached 0.18.0 app, clears quarantine with `xattr -cr`, swaps `app.asar` / `app.asar.unpacked`, then rewrites Info.plist.

| Key | Reconstructed value | Constraint |
| --- | --- | --- |
| `CFBundleIdentifier` | `com.anysphere.sand.reconstructed` | `npm run verify` fails on any other ID |
| `CFBundleDisplayName` | `Grok Bot 0.18 Reconstructed` | Fork display name |
| `CFBundleName` / `CFBundleExecutable` | `Grok Bot` | Electron helper names stay ABI-matched to 0.18 |
| `CFBundleURLTypes` | scheme `sand` only | Inherited aliases such as `grokbot` are removed |
| `ElectronAsarIntegrity` | removed | Stale official integrity metadata is a verify failure |
| Signature | ad-hoc, `--deep --strict` | Official Developer ID / notarization is not retained |
| Default output | `dist/Grok Bot 0.18 Reconstructed.app` | Override with `GROK_BOT_OUTPUT_APP_NAME` |

Packaging also proves the official reference (`verifyOfficialMacReference`) still hashes to the pinned shell and ASAR, then proves the reconstructed package **does not** copy that official `app.asar` or signed Mach-O shell.

Related identity constants in `scripts/lib/config.mjs`: `fidelityBundleId` `com.anysphere.sand.reconstructed.fidelity` (fidelity app) and diagnostic IDs `com.anysphere.sand.reconstructed.fidelity.diagnostic.build*`. Those are not the default package ID.

## Production bindings must resolve into `source/`

Clean host and Electron-main activation is fail-closed. Binding modules must live under reviewed `source/`. Forbidden roots:

- `src/app/`
- `recovered/source-capsules/`
- `dist/deps/`
- `.build/app/`

Overrides:

<ParamField body="GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST" type="string">
Binding manifest path. Default, when present: `manifests/reconstruction/electron-main-production-bindings-manifest.json`.
</ParamField>

<ParamField body="GROK_BOT_HOST_BINDINGS_MANIFEST" type="string">
Host binding manifest. Unset leaves host in `artifact-fallback` until every required slot binds and runner-turn evidence is `supported`.
</ParamField>

Packaged clean activations embed `dist/electron-main-production-bindings.json` / `dist/host-production-bindings.json` with status `validated-clean-source` and empty forbidden input/output graphs. `dist/runtime-composition-audit.json` must match `runtimeComposition` in `dist/reconstruction-build.json`. Clean runtimes must not reach immutable `src/app` evidence.

## Packaging-only safety seams

When Electron main stays on the hydrated artifact, packaging prepends a reconstructed updater guard (idempotent) and enables explicit runtime seams:

```js
process.env.SAND_DISABLE_UPDATES ??= "1";
process.env.SAND_DISABLE_SENTRY ??= "1";
process.env.SAND_DISABLE_TELEMETRY ??= "1";
```

`??=` keeps caller-supplied values. `GROK_BOT_RECONSTRUCTED_DEV=1` (and `GROK_BOT_BUILD_DEV_APP=1` for the dev product name / `sandLab`) can open packaged DevTools and skip the single-instance lock; those seams require exact upstream string anchors or the build throws.

## Commands and verification

```sh
npm run bootstrap   # hydrate ignored src/app/dist
npm run check       # frontend + source typecheck and tests
npm run package     # darwin-only fidelity hybrid app
npm run verify      # required ASAR entries, renderer inventory, bundle ID, codesign
```

`npm run verify` additionally requires at least 1,000 surviving `// src/` evidence markers in hydrated `electron-main` + `host` mains, forbids renderer `.map` files, and requires ASAR paths including `/dist/renderer/index.html`, `/dist/reconstruction-build.json`, and `/dist/runtime-composition-audit.json`. For `checksum-pinned-artifact-runtime`, every packaged `dist/renderer/**` file must match the embedded SHA-256 inventory except the hash-recorded Router patch chunks.

<AccordionGroup>
  <Accordion title="Common boundary failures">
    - Missing runtime: `Missing 0.18.0 runtime. Run npm run bootstrap first.`
    - Version mismatch on `GROK_BOT_018_APP`: `Expected Grok Bot 0.18.0`
    - DMG or `app.asar` digest mismatch (including LFS pointer-sized archives)
    - Hydration missing `dist/electron-main/main.cjs`, `dist/host/host-main.cjs`, or `dist/renderer/index.html`
    - `npm run package` on non-darwin
    - Settings patch anchors missing or duplicated after a renderer override
    - Verify: unexpected `CFBundleIdentifier`, leftover `ElectronAsarIntegrity`, missing `sand` URL scheme, official ASAR copied into the reconstructed app
  </Accordion>
</AccordionGroup>

## Next

<CardGroup>
  <Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
    GROK_BOT_018_APP, cache, LFS DMG, public URL, and src/app/dist hydration checks.
  </Card>
  <Card title="Package the macOS app" href="/package-macos-app">
    Compile runtimes, patch Settings Router, replace app.asar, ad-hoc sign, verify.
  </Card>
  <Card title="Work on the frontend reconstruction" href="/edit-frontend-workspace">
    frontend/ is a Vite workspace; packaged UI stays the patched shipped renderer unless GROK_BOT_RENDERER_SOURCE is set.
  </Card>
  <Card title="Evidence and provenance" href="/evidence-and-provenance">
    Pinned DMG and app.asar identities, evidence-only UI rule, production bindings into source/.
  </Card>
</CardGroup>

---

## 05. Inference router

> SandInferenceProvider ids cursor, claude-code, codex, openrouter; default cursor; local transcript store schemaVersion 2; usage schemaVersion 1.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/05-inference-router.md
- Generated: 2026-08-24T05:51:38.839Z

### Source Files

- `source/shared/inference-router.ts`
- `source/node-agent-coordinator/inference-router.ts`
- `source/host/extensions/inference/provider-session.ts`
- `source/electron-main/main-edge.ts`
- `frontend/src/recovered/features/settings/overlay/router.ts`
- `tests/inference-router-transcript.test.mjs`

---
title: "Inference router"
description: "SandInferenceProvider ids cursor, claude-code, codex, openrouter; default cursor; local transcript store schemaVersion 2; usage schemaVersion 1."
---

The inference router is the shared `SandInferenceProvider` selection that decides whether a turn uses the Cursor-hosted agent path or a reconstructed direct provider. The four ids are `cursor`, `claude-code`, `codex`, and `openrouter`. Missing or invalid values resolve to `cursor`. The packaged Settings → Router page writes the selection through `window.desktop.agent.setInferenceRouter`, persists `inferenceProvider` on `settings.json` (`version: 1`), records usage as `inferenceRouterUsage` with `schemaVersion: 1`, and stores non-Cursor conversation tails in `inference-router-transcript.json` with `schemaVersion: 2`.

<Note>
Cursor remains the default and is not intercepted. Direct providers (`claude-code`, `codex`, `openrouter`) own `sendPrompt`, merge local transcript tails, and execute Grok Bot plugin tools through `listRoutedMcpTools` / `executeRoutedMcpTool`.
</Note>

## Provider ids

| Id | Settings label | Auth surface | Turn path | Plugin tools |
| --- | --- | --- | --- | --- |
| `cursor` | Cursor | Existing Grok Bot / Cursor session | Native host inference session | Native Grok Bot tools and plugins |
| `claude-code` | Claude Code | Local Claude Code CLI login | Claude Agent SDK `query` | HTTP MCP bridge `grok_bot_plugins` |
| `codex` | Codex | Private `~/.codex/auth.json` ChatGPT tokens | Direct Responses at `https://chatgpt.com/backend-api/codex/responses` | Direct tool-execution loop |
| `openrouter` | OpenRouter | `OPENROUTER_API_KEY` (env or `box-secrets.json`) | OpenAI-compatible `https://openrouter.ai/api/v1` | Direct tool-execution loop |

Shared type:

```ts title="source/shared/inference-router.ts"
export const SAND_INFERENCE_PROVIDERS = ["cursor", "claude-code", "codex", "openrouter"] as const;
export type SandInferenceProvider = (typeof SAND_INFERENCE_PROVIDERS)[number];
```

`isSandInferenceProvider` is the only accepted setter check. `setInferenceRouter` throws `Unknown inference provider.` for any other string.

## Runtime ownership

```mermaid
flowchart TB
  subgraph ui [Renderer]
    RouterPanel["Settings → Router"]
    UsagePanel["Settings → Usage & Billing"]
    DesktopAgent["window.desktop.agent"]
  end
  subgraph main [Electron main]
    GetSet["getInferenceRouter / setInferenceRouter"]
    SettingsStore["SandSettingsStore settings.json"]
    LocalStatus["getLocalInferenceCliStatus"]
  end
  subgraph coordinator [Node agent coordinator]
    Dispatch["createCoordinatorInferenceRouter.dispatch"]
    Transcript["inference-router-transcript.json"]
    RemoteGW["gatewayClient.dispatchCommand"]
  end
  subgraph host [Host]
    TurnShell["turn-run-shell / createHostInference"]
    CursorPath["createCursorSandInference"]
    ProviderSession["runRoutedProviderText"]
  end
  RouterPanel --> DesktopAgent
  UsagePanel --> DesktopAgent
  DesktopAgent --> GetSet
  GetSet --> SettingsStore
  GetSet --> LocalStatus
  GetSet -->|"syncHostSettingsToBox({ inferenceProvider })"| host
  Dispatch -->|"provider === cursor"| RemoteGW
  Dispatch -->|"provider !== cursor and sendPrompt"| ProviderSession
  Dispatch --> Transcript
  TurnShell -->|"inferenceProvider === cursor"| CursorPath
  TurnShell -->|"else"| ProviderSession
```

Three layers read the same `settings.json` field:

- Electron main for Settings RPC and local CLI status.
- Host `SettingsService` / `SandSettingsStore` for `createHostInference` and `turn-run-shell`.
- Coordinator `createCoordinatorInferenceRouter` for intercepting renderer `sendPrompt` before the remote gateway.

Cursor turns stay on the original host inference client. Direct providers replace that session with `createProviderPromptSession` / `runRoutedProviderText`.

## Select and persist a provider

Packaged UI does **not** use the frontend design-workspace key `settings.router-provider.v1`. Packaging tests require the shipped renderer patch to call desktop RPC instead.

<ParamField body="provider" type="SandInferenceProvider" required>
One of `cursor`, `claude-code`, `codex`, `openrouter`. Default when unset: `cursor`.
</ParamField>

:::endpoint GET getInferenceRouter
Read the current route, usage snapshot, and local CLI status.

**Channel:** `window.desktop.agent.getInferenceRouter()` → main-edge `getInferenceRouter`

**Returns:** `{ provider, usage, local }`

- `provider` — stored `inferenceProvider`, coerced with `isSandInferenceProvider`, else `cursor`
- `usage` — host `inferenceRouterUsage` when reachable, else the local settings copy
- `local` — `{ codex, "claude-code" }` installation/auth probes

`openrouter` status is not in `local`; the Router panel checks the secrets list for `OPENROUTER_API_KEY`.
:::

:::endpoint POST setInferenceRouter
Persist a provider id and mirror it into host settings.

**Channel:** `window.desktop.agent.setInferenceRouter(provider)` → main-edge `setInferenceRouter` with `{ provider }`

**Behavior:**

1. Reject unknown ids with `Unknown inference provider.`
2. Write `inferenceProvider` through `SandSettingsStore.setInferenceProvider`.
3. Call `syncHostSettingsToBox({ inferenceProvider })`. Host sync failure is swallowed; the local write still stands.
4. Return `{ provider, usage, local }` using host usage when the sync response includes it.

The patched Router panel then dispatches `sand-router-provider-changed` with that payload.
:::

<RequestExample>
```js title="Renderer"
await window.desktop.agent.setInferenceRouter("codex");
```
</RequestExample>

<ResponseExample>
```json title="getInferenceRouter / setInferenceRouter"
{
  "provider": "codex",
  "usage": {
    "schemaVersion": 1,
    "providers": {
      "cursor": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "claude-code": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "codex": { "requests": 3, "inputTokens": 1200, "outputTokens": 400, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": "2026-08-24T12:00:00.000Z" },
      "openrouter": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null }
    }
  },
  "local": {
    "codex": { "installed": true, "authenticated": true, "executablePath": "/opt/homebrew/bin/codex" },
    "claude-code": { "installed": false, "authenticated": false, "executablePath": null }
  }
}
```
</ResponseExample>

`settings.json` lives under the Sand data root (`SAND_DATA_ROOT`, else `<userData>/sand-data`, else `~/.grokbot` for production `sand`). Writes are atomic (`settings.json.<pid>.tmp` then rename). Host `setHostSettings` only accepts a provider that passes `isSandInferenceProvider`.

The Vite `frontend/` overlay still round-trips `{ schemaVersion: 1, provider }` under `settings.router-provider.v1` for the design workspace. That key is not the packaged persistence path.

## Direct-provider intercept

Coordinator dispatch order:

1. `reactToMessage` — toggle a local `by: "me"` reaction on a stored entry if one exists; emit `transcript` `updated`.
2. Transcript reads (`getAgentTranscriptTail`, `openAgentTail`, `getAgentTranscriptWindow`) when provider is not `cursor` — remote entries plus projected local entries, sliced to `limit` (default `500`).
3. `sendPrompt` when provider is not `cursor` — handle locally and return `{ accepted: true, clientNonce, provider }` immediately. Turns for the same `agentId` are serialized on an in-process queue.
4. Otherwise `{ handled: false }` so the remote gateway runs.

```mermaid
sequenceDiagram
  participant Renderer
  participant Coordinator as createCoordinatorInferenceRouter
  participant Transcript as inference-router-transcript.json
  participant Host as runRoutedProviderText
  participant Gateway as gatewayClient
  Renderer->>Coordinator: sendPrompt({ agentId, prompt, richText, clientNonce })
  alt provider is cursor
    Coordinator->>Gateway: dispatchCommand(sendPrompt)
  else provider is claude-code, codex, or openrouter
    Coordinator->>Transcript: append user t{n}u
    Coordinator->>Renderer: transcript appended + agents thinking pulse
    Note over Coordinator: wait 1200 ms for composing row
    Coordinator->>Host: runRoutedProviderText + onTextDelta
    Host-->>Coordinator: streamed text then final content
    Coordinator->>Transcript: append assistant t{n}s0
    Coordinator-->>Renderer: { accepted, clientNonce, provider }
  end
```

Local turn constraints:

| Rule | Value |
| --- | --- |
| Required args | Non-empty `agentId` and `prompt` |
| Optional args | `richText` (string), `clientNonce` (else a new UUID) |
| Turn ids | Next `t{n}` after max of remote and local `/^t(\d+)(?:u\|s\d+)$/` ids |
| User projection | `{ kind: "message", id: "t{n}u", role: "user", ... }` |
| Assistant projection | `{ kind: "send-message", id: "t{n}s0", message: { type: "text", content } }` |
| Activity pulse | `currentActivity: { kind: "thinking" }` every 250 ms until the turn settles |
| Composing delay | 1200 ms before the first assistant stream so the shipped transcript can show composing |
| Failure | Assistant row `Router error: <message>` |

## Local transcript store

Path: `<dataDir>/inference-router-transcript.json`  
Mode: `0o600` temp file, then rename  
Parser: `parseInferenceRouterTranscriptStore` — any root whose `schemaVersion` is not `2` or whose `agents` is not an object becomes `{ schemaVersion: 2, agents: {} }`

```json title="inference-router-transcript.json"
{
  "schemaVersion": 2,
  "agents": {
    "<agentId>": [
      {
        "provider": "codex",
        "role": "user",
        "content": "@Gmail what's new?",
        "richText": "{\"type\":\"doc\",...}",
        "id": "t1u",
        "clientNonce": "nonce-1",
        "timestampMs": 123,
        "reactions": [{ "emoji": "👍", "by": "me" }]
      },
      {
        "provider": "codex",
        "role": "assistant",
        "content": "...",
        "id": "t1s0",
        "timestampMs": 456
      }
    ]
  }
}
```

<ResponseField name="schemaVersion" type="2">
Required. Any other version is discarded.
</ResponseField>
<ResponseField name="agents" type="Record<agentId, StoredEntry[]>">
Per-agent tails. Each agent is truncated to the last 200 valid entries on parse and on append.
</ResponseField>
<ResponseField name="provider" type='"claude-code" | "codex" | "openrouter"'>
Cursor rows are never stored here.
</ResponseField>
<ResponseField name="richText" type="string">
Optional. Must be a string; object carriers are dropped. Used to keep structured MCP mention docs across reload.
</ResponseField>

Malformed rows are skipped, not migrated. `richText` that is not a string rejects the whole entry.

## Usage store

`inferenceRouterUsage` on `settings.json` is `schemaVersion: 1` and always has all four provider keys.

```ts title="SandInferenceRouterUsageProvider"
{
  requests: number;
  inputTokens: number;
  outputTokens: number;
  cacheReadTokens: number;
  cacheWriteTokens: number;
  lastUsedAt: string | null;
}
```

`SandSettingsStore.recordInferenceUsage` increments `requests` by 1, adds finite non-negative token counts (rounded), and sets `lastUsedAt` to `new Date().toISOString()`. Direct providers record from streamed usage; the Cursor host path records from `extendedUsage` on the wrapped executor.

These counters are local activity records. They are not a provider invoice. The patched Usage & Billing page shows **Tracked activity** for the current route plus any provider with `requests > 0`, and still mounts the original Cursor usage panel when the current route is `cursor`.

## Provider clients

Direct turns share this system prompt:

> You are Grok Bot, a warm, concise desktop assistant. You are running inside Grok Bot, not inside Codex CLI or Claude Code. The tools supplied with this request are Grok Bot's already-connected plugins and accounts.

| Provider | Client | Model default | Credential check | Tool max steps |
| --- | --- | --- | --- | --- |
| `claude-code` | `@anthropic-ai/claude-agent-sdk` `query` | `SAND_CLAUDE_MODEL` if set, else Claude Code default | `resolveClaudeCodeCliPath()` must find a binary | 8 with MCP URL, else 1 |
| `codex` | `streamCodexDirectResponses` | `SAND_CODEX_MODEL`, else `model` in `config.toml`, else `gpt-5.4` | Regular non-symlink `auth.json` with mode bits `0o077 == 0`, `auth_mode === "chatgpt"`, and access/refresh/id/account tokens | 8 with tools, else 1 |
| `openrouter` | `@ai-sdk/openai` `streamText` | `SAND_OPENROUTER_MODEL` or `openai/gpt-5.2` | `OPENROUTER_API_KEY` from env, else `box-secrets.json` | 8 with tools, else 1 |

`claude-code` MCP: coordinator starts `createRoutedMcpBridge` on `127.0.0.1:<ephemeral>/mcp/<secret>` and passes that URL as `mcpServers.grok_bot_plugins`. Allowed tools: `mcp__grok_bot_plugins__*`. `persistSession` is `false`.

`codex` / `openrouter`: coordinator lists tools with `listRoutedMcpTools` and executes with `executeRoutedMcpTool` (`providerIdentifier`, `name`, `toolName`, `args`, `toolCallId`, `agentId`).

Codex HTTP uses `Authorization: Bearer <access_token>` and `ChatGPT-Account-Id`. A `401` refreshes against `https://auth.openai.com/oauth/token` and rewrites `auth.json` atomically at mode `0o600`.

## Local CLI status

`getLocalInferenceCliStatus()` is what Settings shows as Ready / Sign in / Not installed.

| Probe | `installed` | `authenticated` | Path resolution |
| --- | --- | --- | --- |
| `codex` | `auth.json` exists | Usable ChatGPT token file (same privacy rules as the executor) | `CODEX_PATH`, `~/.local/bin/codex`, `~/.codex/bin/codex`, `PATH`, Homebrew paths. The CLI binary is **not** on the request path. |
| `claude-code` | Resolved `claude` binary | `~/.claude/.credentials.json` exists **or** `ANTHROPIC_API_KEY` is non-empty | `CLAUDE_CODE_PATH`, `~/.local/bin/claude`, `~/.claude/local/claude`, `PATH`, Homebrew paths |

`CODEX_HOME` overrides the Codex home used for `auth.json` and `config.toml` (default `~/.codex`).

## Errors the router emits

| Condition | Message |
| --- | --- |
| Unknown `setInferenceRouter` id | `Unknown inference provider.` |
| Direct `sendPrompt` missing `agentId` or `prompt` | `Local inference routing requires an agentId and prompt` |
| OpenRouter key missing | `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.` |
| Claude Code binary missing | `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.` |
| Codex `auth.json` not a private regular file | `Codex login credentials must be a private direct regular file.` |
| Codex not ChatGPT-signed-in | `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.` |
| Codex refresh failed | `Codex login expired and could not be refreshed. Run \`codex login\` again.` |
| Claude Code query without a success result | `Claude Code ended without a result.` or joined `final.errors` |

Failed direct turns still persist an assistant `Router error: …` row so the shipped transcript can show the failure.

## Verification

Router identity and intercept wiring are asserted by:

- `tests/router-settings.test.mjs` — recovered overlay ids, default `cursor`, unknown preference falls back to `cursor`
- `tests/inference-router-transcript.test.mjs` — schemaVersion 2 `richText` round-trip; object `richText` dropped
- `tests/publication-packaging.test.mjs` — desktop RPC, `settings.json` sync, transcript merge methods, MCP bridge bind `127.0.0.1`, Codex Responses URL, OpenRouter base URL, 1200 ms composing delay

## Next

<CardGroup>
  <Card title="Choose an inference provider" href="/choose-inference-provider">
    Set Settings → Router, persist `inferenceProvider`, and satisfy Cursor, Claude Code, Codex, or OpenRouter credentials.
  </Card>
  <Card title="Provider clients" href="/provider-clients">
    Cursor default path, Claude Agent SDK query, Codex Responses, OpenRouter model `openai/gpt-5.2`.
  </Card>
  <Card title="Route Grok Bot plugin tools" href="/route-mcp-tools">
    `listRoutedMcpTools` and `executeRoutedMcpTool` across the Claude Code MCP bridge versus Codex/OpenRouter direct execution.
  </Card>
  <Card title="Settings schema" href="/settings-schema">
    `settings.json` version 1 fields including `inferenceProvider` and `inferenceRouterUsage`.
  </Card>
  <Card title="Desktop RPC" href="/desktop-rpc">
    `window.desktop.agent` `getInferenceRouter` / `setInferenceRouter` and secrets channels.
  </Card>
  <Card title="Router and sandbox failures" href="/router-failures">
    Unknown provider, private `auth.json`, missing Claude Code, missing `OPENROUTER_API_KEY`.
  </Card>
</CardGroup>

---

## 06. Box runtime

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

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/06-box-runtime.md
- Generated: 2026-08-24T05:51:13.115Z

### 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>

---

## 07. Evidence and provenance

> Pinned 0.18.0 DMG and app.asar SHA-256 identities, evidence-only UI rule, and production bindings that must resolve into source/.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/07-evidence-and-provenance.md
- Generated: 2026-08-24T05:53:50.077Z

### Source Files

- `PROVENANCE.md`
- `NOTICE.md`
- `frontend/README.md`
- `manifests/reconstruction/electron-main-production-bindings-manifest.json`
- `tests/publication-bootstrap.test.mjs`
- `scripts/audit-ui-provenance.mjs`

---
title: "Evidence and provenance"
description: "Pinned 0.18.0 DMG and app.asar SHA-256 identities, evidence-only UI rule, and production bindings that must resolve into source/."
---

The reconstruction treats the public Grok Bot **0.18.0** macOS arm64 release as an immutable product specification. `scripts/lib/config.mjs` pins `upstreamVersion` to `0.18.0`, `dmgSha256` to `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb`, and `upstreamAsarSha256` to `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58`. Bootstrap, hydration, packaging, and `npm run verify` reject any other DMG or `app.asar` identity. Recovered UI and Electron-main production bindings may express only behavior that those artifacts still contain.

## Pinned 0.18.0 identities

| Identity | Value |
| --- | --- |
| Product | Grok Bot |
| Version | `0.18.0` |
| Upstream bundle ID | `com.anysphere.sand` |
| Reconstructed bundle ID | `com.anysphere.sand.reconstructed` |
| Electron | `42.1.0` |
| macOS DMG | `https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg` |
| DMG SHA-256 | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| DMG size | `155793020` bytes |
| Original `app.asar` SHA-256 | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |
| Windows installer | `https://downloads.cursor.com/grokbot/stable/win32-x64/0.18.0/Grok_Bot_0.18.0_Setup.exe` |
| Windows SHA-256 | `464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e` |
| Windows size | `125825552` bytes |
| Preservation manifest | `research-archives/original/0.18.0/artifacts.json` (`schemaVersion` `1`) |

The original application was Developer ID signed and notarized by Anysphere Incorporated. Reconstructed builds use `com.anysphere.sand.reconstructed`, are ad-hoc signed only, and do not retain or claim the upstream signature.

Git LFS preserves both installers. `artifacts.json` is the machine-readable inventory; `SHA256SUMS` is the `shasum -c` companion.

:::files
research-archives/original/0.18.0/
├── artifacts.json
├── SHA256SUMS
├── macos-arm64/Grok_Bot_0.18.0.dmg
└── windows-x64/Grok_Bot_0.18.0_Setup.exe
:::

```sh
git lfs install
git lfs pull
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
```

<Warning>
No upstream source-code license is implied. Do not present reconstructed material as original source or an official build. The LFS installers remain subject to their own terms and are not covered by any license applied to reconstructed code. Complete an independent rights review before public redistribution.
</Warning>

## Provenance layers

Checksums, UI catalogs, and production bindings are separate fail-closed layers. A green typecheck does not substitute for any of them.

```mermaid
flowchart TB
  subgraph installers [Pinned public installers]
    DMG["Grok_Bot_0.18.0.dmg<br/>dmgSha256"]
    EXE["Grok_Bot_0.18.0_Setup.exe<br/>artifacts.json"]
  end
  subgraph payload [Hydrated immutable payload]
    ASAR["Contents/Resources/app.asar<br/>upstreamAsarSha256"]
    DIST["src/app/dist<br/>electron-main / host / renderer"]
  end
  subgraph reviewed [Reviewed reconstruction]
    SOURCE["source/"]
    FRONTEND["frontend/"]
    MANIFEST["manifests/reconstruction/<br/>electron-main-production-bindings-manifest.json"]
    CATALOG["frontend/manifests/<br/>ui-evidence-anchors.json"]
  end
  DMG --> ASAR
  ASAR --> DIST
  DIST -->|"artifactAnchor needles"| MANIFEST
  DIST -->|"immutable renderer claims"| CATALOG
  MANIFEST -->|"module must resolve under source/"| SOURCE
  CATALOG -->|"exact UI claim in cleanPath"| FRONTEND
```

## Bootstrap and hydration gates

`npm run bootstrap` (`scripts/bootstrap-runtime.mjs`) locates a 0.18.0 app, then hydrates ignored `src/app/dist` from its `app.asar`.

<Steps>
<Step title="Resolve a 0.18.0 app">
Order: `GROK_BOT_018_APP` (copied into `.cache/runtime/Grok Bot.app`), then the cached app, then the LFS DMG, then `dmgUrl`. `validateRuntimeApp` requires `CFBundleShortVersionString` `0.18.0`, `Contents/MacOS/Grok Bot`, and `Contents/Resources/app.asar.unpacked`.
</Step>
<Step title="Verify the DMG">
Cached and downloaded DMGs must equal `dmgSha256`. An archived LFS pointer or drifted file fails with `Archived DMG checksum mismatch: expected a253ccd8…`. A public download that does not match is deleted as `*.partial` and fails with `DMG checksum mismatch`.
</Step>
<Step title="Hydrate src/app/dist">
`hydrateSourcePayloadFromAsar` hashes the ASAR against `upstreamAsarSha256` (overrideable only in tests via `expectedSha256`). Mismatch throws `Upstream app.asar checksum mismatch`. The archive must contain `dist/electron-main/main.cjs`, `dist/host/host-main.cjs`, and `dist/renderer/index.html` before `dist/` is copied to `src/app/dist`.
</Step>
</Steps>

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute path to an installed 0.18.0 `Grok Bot.app`. Version mismatch aborts before hydration.
</ParamField>

<ParamField body="GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST" type="string">
Optional schemaVersion 1 binding manifest. Defaults to `manifests/reconstruction/electron-main-production-bindings-manifest.json` when that file exists. A residual-only manifest cannot override the sixteen reviewed `source/` providers.
</ParamField>

Success log:

```text
Checksum-pinned source payload ready: <repo>/src/app/dist (6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58)
```

Packaged artifact-renderer provenance must also carry `upstreamAppAsarSha256` equal to that digest. `npm run verify` rejects a drifted provenance identity.

## Evidence-only reconstruction rule

The immutable release is the product specification. Recovered source may express only behavior supported by at least one inspectable artifact anchor:

- emitted code or source-path markers
- extracted capsules / source maps
- shipped strings, assets, or CSS
- renderer DOM signatures
- IPC / RPC contracts
- repeatable observation of the shipped runtime

The rule is strict for the renderer. Do not invent or redesign a screen, route, control, label, selector, state, or interaction to fill an evidence gap. A clean abstraction or test seam is acceptable only when it preserves artifact-derived semantics and does not add product behavior. When evidence is incomplete, record the uncertainty and leave the feature unmapped or evidence-only.

Passing `npm run typecheck` / `npm run frontend:build` is not proof of provenance. Speculative UI is a release-blocking defect.

The shipped renderer is optimized production bundles, not authored frontend source or source maps. `frontend/` is a partial evidence-backed reconstruction and Vite design workspace. Packaged builds retain the pinned renderer and apply only a narrow, hash-recorded Settings Router transform unless `GROK_BOT_RENDERER_SOURCE` overrides the renderer source.

Comments beginning with `@evidence` (or `@artifact-evidence`) are provenance annotations, not imports. `scripts/audit-ui-provenance.mjs` accepts annotation paths only under:

- `src/app/`
- `recovered/frontend/`
- `recovered/source-capsules/`
- `frontend/manifests/`

## UI evidence catalog and auditor

`frontend/manifests/ui-evidence-anchors.json` is `schemaVersion` `1` with an `entries` array. Each entry names one clean file and exact anchors.

Allowed catalog `category` values: `visible-string`, `selector`, `route`, `dom-signature`, `asset`.

Each anchor must:

- use an exact value (no wildcards, newlines, or prose such as `see shipped`)
- point `artifact` at immutable `src/app/` evidence (not the clean file or the catalog)
- occur at the declared artifact location
- match an extracted UI claim in the named `cleanPath`
- if `registry` is set, name a reviewed registry: `recovered/frontend/reports/features.json`, `frontend/manifests/semantic-symbols.json`, `frontend/manifests/component-names.json`, `frontend/manifests/conversation-evidence.json`, or `frontend/src/production/evidence.ts`

Auditor inputs (`UI_AUDIT_INPUTS`):

| Role | Path |
| --- | --- |
| Anchor catalog | `frontend/manifests/ui-evidence-anchors.json` |
| Clean roots | `frontend/src/recovered`, `frontend/src/production`, plus `frontend/src/main.tsx` |
| Immutable renderer | `src/app/dist/renderer` |
| Production evidence registry | `frontend/src/production/evidence.ts` |
| Optional recovered trees | `recovered/source-capsules/manifest.json`, `recovered/frontend/app`, `recovery/full-app-coverage.json` |

```sh
node scripts/audit-ui-provenance.mjs [--json] [--include-audits] [--include-dev] [--source=substring] [--limit=N] [--fail-on=high|medium]
```

`--fail-on=high` exits `1` when `summary.high > 0`. `--fail-on=medium` also fails on medium findings.

High-severity codes include `missing-module-provenance`, `unanchored-visible-string`, `unanchored-route`, `invalid-evidence-annotation`, `catalog-anchor-not-in-artifact`, `catalog-self-or-nonimmutable-reference`, and `catalog-vague-anchor`. Distinctive reconstructed selectors matching `/^\.(?:sand-|grok-|recovered-)/` are high; other unanchored selectors are medium.

When a full recovered workspace is present, `scripts/renderer-production-build.mjs` requires `ui.summary.catalogErrors === 0` and `ui.summary.findings === 0` plus renderer closure covering **5** composed feature surfaces and **11** shipped routes. On the publication tree it instead validates the checked-in catalog against the closure report and omits `recovered/` registries.

`npm run verify` still requires at least **1,000** surviving `// src/` evidence source markers across hydrated `dist/electron-main/main.cjs` and `dist/host/host-main.cjs`.

## Electron-main production bindings

Clean Electron-main activation is fail-closed on sixteen slots. The checked-in map is `manifests/reconstruction/electron-main-production-bindings-manifest.json` (`schemaVersion` `1`).

`tests/publication-bootstrap.test.mjs` requires every `module` to match `^\.\./\.\./source/` relative to that manifest directory, resolve under repository `source/`, and exist on disk.

Activation (`scripts/electron-main-production-activation.mjs`) additionally:

- requires `classification` `generated-source` | `third-party` | `native` and `access` `call` | `value`
- requires every `artifactAnchor.artifact` to be `src/app/dist/electron-main/main.cjs` with an integer `line` and the exact needle from `electronMainProductionBindingEvidence`
- forbids generated-source modules under `src/app/`, `recovered/source-capsules/`, `dist/deps/`, and `.build/app/`
- validates the export with an esbuild import of the named symbol
- writes packaged provenance to `dist/electron-main-production-bindings.json`

A full sixteen-slot manifest is accepted for compatibility. A partial residual manifest must not overlap the reviewed inventory. Missing slots return `incomplete-evidence-derived-manifest`; there is no default or native fallback.

| `path` | `export` | `access` | Needle |
| --- | --- | --- | --- |
| `adapters.secureStorage` | `createElectronProductionSecureStorageBinding` | `call` | `initializeSecureStorage();` |
| `adapters.settings` | `createElectronProductionSettingsBinding` | `call` | `var sandSettingsStore = new SandSettingsStore(` |
| `adapters.attachmentGateway` | `createElectronProductionAttachmentGatewayBinding` | `call` | `attachments: createAttachmentEdgePort({` |
| `adapters.mainRpc` | `createElectronProductionMainRpcBinding` | `call` | `const mainEdge = serveMainEdge({` |
| `adapters.updaterInstaller` | `createElectronProductionUpdaterInstallerBinding` | `call` | `updateService = createUpdateServiceWiring({` |
| `adapters.mediaProtocol` | `createElectronProductionMediaProtocolBinding` | `call` | `registerSandMediaProtocol();` |
| `adapters.accountOAuth` | `createElectronProductionAccountOAuthBinding` | `call` | `var cursorAuthWiring = createCursorAuthWiring({` |
| `adapters.experiments` | `createElectronProductionExperimentsBinding` | `call` | `var experimentsRuntime = createExperimentsRuntime({` |
| `adapters.mcpOAuth` | `createProductionMcpOAuthAdapter` | `call` | `var mcpRuntime = createMcpRuntime({` |
| `adapters.telemetry` | `createElectronProductionTelemetryBinding` | `call` | `desktopTelemetry = await SandDesktopStructuredLogTelemetry.create({` |
| `adapters.notifications` | `createElectronProductionNotificationsBinding` | `call` | `const osNotificationManager = new SandOsNotificationManager({` |
| `adapters.coordinator` | `createElectronProductionCoordinatorBinding` | `value` | `const createCoordinatorSession = () => createCoordinatorRuntime({` |
| `adapters.ipc` | `createElectronProductionIpcBinding` | `call` | `registerSecretsIpc({` |
| `startup` | `createElectronProductionStartupBinding` | `call` | `var desktopStartup = createDesktopStartupTracker({` |
| `parseAllowedExternalUrl` | `parseAllowedExternalUrl` | `value` | `function parseAllowedExternalUrl(value)` |
| `reportFailure` | `reportDesktopEdgeFailure` | `value` | `function reportDesktopEdgeFailure(area, leg, error6)` |

`npm run verify` requires packaged `dist/electron-main-production-bindings.json` (and the host analog) to have `status` `validated-clean-source`, empty `executableGraph.forbiddenInputs` / `forbiddenOutputReferences`, and no binding `module` containing `src/app`, `dist/deps`, or `recovered/source-capsules`.

Hydration checksum test: `hydrateSourcePayloadFromAsar(..., { expectedSha256: "0".repeat(64) })` must reject with `/checksum mismatch/`.

## Rights and publication

`NOTICE.md` states this tree is an unofficial reconstruction, not affiliated with or endorsed by Anysphere, Cursor, xAI, or SpaceX. Absence of the original binary payload from Git does not by itself make the reconstruction safe to redistribute. `npm run publication:check` is the archive/init/add tree gate before a public remote; it does not replace a rights review.

## Next

<CardGroup>
<Card title="Reconstruction boundary" href="/reconstruction-boundary">
Compiled `source/` runtimes, checksum-pinned shipped renderer, and `com.anysphere.sand.reconstructed`.
</Card>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
`npm run bootstrap` order, DMG/`app.asar` SHA-256 checks, and `src/app/dist` hydration.
</Card>
<Card title="Preserve original installers" href="/preserve-original-installers">
Git LFS 0.18.0 identities, `artifacts.json` schemaVersion 1, and `shasum -c SHA256SUMS`.
</Card>
<Card title="Work on the frontend reconstruction" href="/edit-frontend-workspace">
`frontend/` as a Vite design workspace; packaged UI stays the patched shipped renderer.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
Required ASAR paths, evidence markers, and packaged binding provenance.
</Card>
<Card title="Publication export" href="/publication-export">
Tree equality, LFS installer push, and NOTICE/rights review before a public remote.
</Card>
</CardGroup>

---

## 08. Choose an inference provider

> Set Settings → Router, persist inferenceProvider, and satisfy Cursor session, Claude Code login, Codex auth.json, or OPENROUTER_API_KEY.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/08-choose-an-inference-provider.md
- Generated: 2026-08-24T05:51:28.441Z

### Source Files

- `source/host/extensions/inference/provider-session.ts`
- `source/shared/node/inference-router-local.ts`
- `source/electron-main/main-edge.ts`
- `scripts/lib/router-renderer-patch.mjs`
- `frontend/src/recovered/features/settings/overlay/router.ts`
- `tests/router-settings.test.mjs`

---
title: "Choose an inference provider"
description: "Set Settings → Router, persist inferenceProvider, and satisfy Cursor session, Claude Code login, Codex auth.json, or OPENROUTER_API_KEY."
---

The packaged app’s **Settings → Router** panel selects a `SandInferenceProvider` (`cursor`, `claude-code`, `codex`, `openrouter`), persists it as `inferenceProvider` on `settings.json` version `1` (default `cursor`), and uses that value for new turns. The overlay is injected at package time into the checksum-pinned renderer. `window.desktop.agent.setInferenceRouter(provider)` writes the store and mirrors `{ inferenceProvider }` into the box; it does not validate credentials. Auth is checked when a turn runs.

<Frame caption="Settings → Router: provider dropdown, Computer toggle, account status, and per-provider usage">
![Settings Router overlay](/docs/assets/router-settings.png)
</Frame>

<Note>
The Vite workspace under `frontend/` models the same four ids and a local key `settings.router-provider.v1`. Packaged UI does not use that key. Preference for the running app is `inferenceProvider` in host `settings.json`.
</Note>

## Prerequisites

- A packaged reconstructed app (`npm run package` applies the Settings registry and Router panel patches).
- For `claude-code` or `codex`, the matching local login on this Mac (no extra API key in Settings).
- For `openrouter`, an OpenRouter key in the environment or saved through **Settings → Router**.

## Choose a provider

<Steps>
<Step title="Open Settings → Router">
In the packaged app, open Settings and select **Router** (`id: "router"`, icon `git-branch`). The panel loads `window.desktop.agent.getInferenceRouter()` and listens for `sand-router-provider-changed`.
</Step>
<Step title="Select Provider">
Use the **Provider** dropdown (`aria-label="Routing provider"`). Allowed values: `cursor`, `claude-code`, `codex`, `openrouter`. Unknown ids fail with `Unknown inference provider.` and the previous selection is restored in the overlay.
</Step>
<Step title="Satisfy account or key">
Confirm the **Account** / **OpenRouter account** row:

| Provider | Ready signal |
| --- | --- |
| `cursor` | Status **Signed in** (Cursor session already attached to Grok Bot) |
| `claude-code` | **Ready** (CLI found and login present) |
| `codex` | **Ready** (usable private `auth.json`) |
| `openrouter` | Key saved: placeholder **Replace saved key**, or `OPENROUTER_API_KEY` in the process environment |

If Claude Code or Codex is missing, the row shows **Not installed** or **Sign in with claude** / **Sign in with codex login**. Install or log in, then reopen Grok Bot so `getLocalInferenceCliStatus()` refreshes.
</Step>
<Step title="Send a new turn">
The next agent turn reads `SandSettingsStore.getInferenceProvider()`. `cursor` stays on the Cursor session path. Any other id uses `createProviderPromptSession(provider)`.
</Step>
</Steps>

<Warning>
Changing the dropdown only persists the id. A turn against `claude-code`, `codex`, or `openrouter` without a usable login or key fails at request time, not at save time.
</Warning>

The same Router page includes **Use local Docker VM**. That toggle is `boxRuntime`, not `inferenceProvider`. See [Enable the local Docker sandbox](/enable-local-docker).

## Provider ids

| `inferenceProvider` | Label | Auth surface | New-turn path |
| --- | --- | --- | --- |
| `cursor` | Cursor | Existing Grok Bot / Cursor session | Host Cursor session (`createSession`) |
| `claude-code` | Claude Code | Claude Code CLI + login | `@anthropic-ai/claude-agent-sdk` `query` |
| `codex` | Codex | ChatGPT tokens in Codex `auth.json` | Direct HTTP to `https://chatgpt.com/backend-api/codex/responses` (CLI not on the request path) |
| `openrouter` | OpenRouter | `OPENROUTER_API_KEY` | OpenAI-compatible `https://openrouter.ai/api/v1` |

<Tabs>
<Tab title="cursor">
Default. Overlay description: “Use your signed-in Cursor account.” Status is always **Signed in**; the Router panel does not probe Cursor auth. Tools stay on native Grok Bot plugins.
</Tab>
<Tab title="claude-code">
Resolve the `claude` executable, then sign in.

Search order for the CLI: `CLAUDE_CODE_PATH`, `~/.local/bin/claude`, `~/.claude/local/claude`, `PATH`, `/opt/homebrew/bin/claude`, `/usr/local/bin/claude`.

Status **authenticated** when `~/.claude/.credentials.json` exists or `ANTHROPIC_API_KEY` is non-empty. A turn still requires the executable; missing CLI throws `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.`

Optional model: `SAND_CLAUDE_MODEL`. Plugin tools go through the Grok Bot MCP bridge (`mcp__grok_bot_plugins__*`).
</Tab>
<Tab title="codex">
Grok Bot reads tokens and calls ChatGPT itself. The `codex` binary is only used for install/status.

Auth file: `$CODEX_HOME/auth.json` if `CODEX_HOME` is set, otherwise `~/.codex/auth.json`. The file must be a regular file (not a symlink) with no group/other bits (`mode & 0o077 === 0`). Required fields: `auth_mode === "chatgpt"` and non-empty `tokens.access_token`, `refresh_token`, `id_token`, `account_id`.

Otherwise: `Codex login credentials must be a private direct regular file.` or `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.`

401 responses refresh at `https://auth.openai.com/oauth/token` and rewrite `auth.json` atomically (`mode 0o600`). Model: `SAND_CODEX_MODEL`, else `model` in `config.toml`, else `gpt-5.4`. Reasoning: `SAND_CODEX_REASONING_EFFORT` or `model_reasoning_effort` in that TOML (`minimal` \| `low` \| `medium` \| `high` \| `xhigh`).
</Tab>
<Tab title="openrouter">
Credential: `process.env.OPENROUTER_API_KEY`, else `OPENROUTER_API_KEY` in `box-secrets.json`. Missing key: `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.`

Save from the overlay with `window.desktop.secrets.upsert({ OPENROUTER_API_KEY })` (`sand:secrets-upsert`). Environment wins over the secrets file.

Model: `SAND_OPENROUTER_MODEL` or `openai/gpt-5.2`. Tools run in the Grok Bot execution loop (`maxSteps` 8 when tools are present).
</Tab>
</Tabs>

## Persist path

```mermaid
flowchart TB
  subgraph renderer [Packaged Settings overlay]
    Panel["RRouterPanel Provider dropdown"]
    Event["sand-router-provider-changed"]
  end
  subgraph electron [Electron main edge]
    GetIR["getInferenceRouter"]
    SetIR["setInferenceRouter"]
    Local["getLocalInferenceCliStatus"]
  end
  subgraph store [Host settings]
    File["settings.json version 1"]
    Field["inferenceProvider"]
    Usage["inferenceRouterUsage schemaVersion 1"]
  end
  subgraph turns [Next turn]
    Cursor["cursor createSession"]
    Routed["createProviderPromptSession"]
  end
  Panel --> SetIR
  SetIR --> Field
  Field --> File
  SetIR -->|"syncHostSettingsToBox best-effort"| File
  GetIR --> Field
  GetIR --> Usage
  GetIR --> Local
  GetIR --> Panel
  SetIR --> Event
  Field -->|"cursor"| Cursor
  Field -->|"claude-code, codex, openrouter"| Routed
```

<ParamField body="inferenceProvider" type="SandInferenceProvider" required>
Stored on `settings.json`. Allowed: `cursor` \| `claude-code` \| `codex` \| `openrouter`. Omitted or invalid values load as `cursor`. Atomic write: `settings.json.<pid>.tmp` then `rename`.
</ParamField>

Typical packaged path is `~/.grokbot/settings.json` (`getSandProductionRootDir`). Overrides: absolute `SAND_DATA_ROOT`, or `SAND_USER_DATA_DIR` / `--user-data-dir` (then `sand-data/settings.json`). Unpackaged variant uses `~/.cursor/<variant>/settings.json`.

`setInferenceRouter` always updates the local store first. `syncHostSettingsToBox({ inferenceProvider })` failures are swallowed; the RPC still returns the local provider, usage, and CLI status.

## Desktop RPC

Preload: `window.desktop.agent.getInferenceRouter()` and `window.desktop.agent.setInferenceRouter(provider)` (`setInferenceRouter` args: `{ provider }`).

:::endpoint GET getInferenceRouter
Load current route, local usage, and Claude/Codex CLI status.

**Returns**

<ResponseField name="provider" type="SandInferenceProvider">
Current `inferenceProvider`, or `cursor` if the store value is not a known id.
</ResponseField>
<ResponseField name="usage" type="SandInferenceRouterUsage | null">
Box copy of `inferenceRouterUsage` when readable, else the local store. `schemaVersion` is `1`.
</ResponseField>
<ResponseField name="local" type="{ codex: LocalInferenceCliStatus, 'claude-code': LocalInferenceCliStatus }">
`installed`, `authenticated`, `executablePath` for Codex and Claude Code.
</ResponseField>
:::

:::endpoint POST setInferenceRouter
Persist a provider id.

**Body**

<ParamField body="provider" type="string" required>
Must pass `isSandInferenceProvider`. Otherwise the edge throws `Unknown inference provider.`
</ParamField>

**Returns** the same `{ provider, usage, local }` shape as get.
:::

<RequestExample>
```js title="Renderer overlay"
const current = await window.desktop.agent.getInferenceRouter();
const next = await window.desktop.agent.setInferenceRouter("openrouter");
window.dispatchEvent(new CustomEvent("sand-router-provider-changed", { detail: next }));
await window.desktop.secrets.upsert({ OPENROUTER_API_KEY: key });
```
</RequestExample>

<ResponseExample>
```json title="getInferenceRouter / setInferenceRouter"
{
  "provider": "openrouter",
  "usage": {
    "schemaVersion": 1,
    "providers": {
      "cursor": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "claude-code": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "codex": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "openrouter": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null }
    }
  },
  "local": {
    "codex": { "installed": true, "authenticated": true, "executablePath": "/opt/homebrew/bin/codex" },
    "claude-code": { "installed": true, "authenticated": true, "executablePath": "/opt/homebrew/bin/claude" }
  }
}
```
</ResponseExample>

Routed system prompt (Claude, Codex, OpenRouter) states the process is Grok Bot, not Codex CLI or Claude Code, and that connected plugins are already available.

## Environment and secrets

| Variable | Role |
| --- | --- |
| `OPENROUTER_API_KEY` | Preferred OpenRouter credential |
| `SAND_OPENROUTER_MODEL` | OpenRouter model id; default `openai/gpt-5.2` |
| `CLAUDE_CODE_PATH` | Explicit `claude` executable |
| `ANTHROPIC_API_KEY` | Counts as Claude Code **authenticated** for status only |
| `SAND_CLAUDE_MODEL` | Optional Claude Agent SDK model |
| `CODEX_HOME` | Directory for `auth.json` and `config.toml` (default `~/.codex`) |
| `CODEX_PATH` | Codex CLI path for status only |
| `SAND_CODEX_MODEL` | Codex model override |
| `SAND_CODEX_REASONING_EFFORT` | Codex reasoning override |
| `SAND_DATA_ROOT` / `SAND_USER_DATA_DIR` | Host settings root |

## Usage & Billing

**Usage & Billing** is patched to `RRouterUsage`. It lists the current provider plus any provider with `requests > 0`. Counters (`requests`, `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `lastUsedAt`) are local activity, not a provider invoice. `cursor` still renders the original Cursor usage panel (`Na`) under the router summary.

## Verify

<Check>
After a successful set, reopen **Settings → Router**: the dropdown matches the saved id, **Usage for \<label\>** tracks that provider, and a new chat turn follows Cursor vs `createProviderPromptSession`.
</Check>

<AccordionGroup>
<Accordion title="Turn-time errors">
- `Unknown inference provider.` — RPC `provider` not in `SAND_INFERENCE_PROVIDERS`.
- `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.`
- `Claude Code ended without a result.` / `Claude Code failed (<subtype>).`
- `Codex login credentials must be a private direct regular file.`
- `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.`
- `Codex login expired and could not be refreshed. Run \`codex login\` again.`
- `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.`
</Accordion>
</AccordionGroup>

Full failure catalog: [Router and sandbox failures](/router-failures).

## Next

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, default `cursor`, transcript `schemaVersion` 2, usage `schemaVersion` 1.
</Card>
<Card title="Provider clients" href="/provider-clients">
Cursor path, Claude Agent SDK, Codex Responses URL, OpenRouter default `openai/gpt-5.2`.
</Card>
<Card title="Settings schema" href="/settings-schema">
`settings.json` version 1, `inferenceProvider`, usage, atomic persist.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
`getInferenceRouter`, `setInferenceRouter`, secrets upsert/list.
</Card>
<Card title="Route MCP tools" href="/route-mcp-tools">
Claude Code MCP bridge vs Codex/OpenRouter direct tool execution.
</Card>
<Card title="Enable local Docker" href="/enable-local-docker">
Computer toggle on the same Router page.
</Card>
</CardGroup>

---

## 09. Enable the local Docker sandbox

> Toggle Use local Docker VM, start grok-bot-local-vm, wait for http://127.0.0.1:1340/health, and roll back to remote on start failure.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/09-enable-the-local-docker-sandbox.md
- Generated: 2026-08-24T05:53:07.603Z

### Source Files

- `source/electron-main/box/local-docker-host-connector.ts`
- `source/shared/box-runtime.ts`
- `source/electron-main/main-edge.ts`
- `source/electron-main/box/box-host-connector.ts`
- `scripts/lib/router-renderer-patch.mjs`
- `source/shared/node/settings/sand-settings-store.ts`

---
title: "Enable the local Docker sandbox"
description: "Toggle Use local Docker VM, start grok-bot-local-vm, wait for http://127.0.0.1:1340/health, and roll back to remote on start failure."
---

Settings → Router → **Computer** owns the local sandbox. The **Use local Docker VM** switch calls `window.desktop.agent.setBoxRuntime` with `mode` `"local-docker"` or `"remote"`. Main persists `boxRuntime` in `settings.json`, starts or stops container `grok-bot-local-vm`, waits for `GET http://127.0.0.1:1340/health` with the stored gateway token, then restarts the coordinator. A start or stop failure writes the opposite `boxRuntime` and does not restart the coordinator. Default remains `"remote"`.

<Frame caption="Settings → Router overlay. The Computer card with Use local Docker VM is injected on this page.">
<img src="docs/assets/router-settings.png" alt="Settings Router overlay with provider, account, and usage cards" />
</Frame>

<Info>
Docker Desktop (or another local `docker` daemon) is optional at install time and required only for this toggle. Inference provider choice is independent: Cursor, Claude Code, Codex, and OpenRouter keep their own auth paths.
</Info>

## Prerequisites

- Packaged reconstructed macOS app (the connector refuses a stock image host if `host-main.cjs` and `box-exec-daemon/main.cjs` are missing).
- Docker daemon reachable as the `docker` CLI (`docker info`).
- Loopback ports `1337`, `1339`, `1340`, `6080`, `6081`, and `8790` free on `127.0.0.1`.
- No unowned container already named `grok-bot-local-vm`.

<Warning>
The run uses `--platform linux/amd64`. On Apple Silicon, Docker must be able to pull and run that image. The connector binds published ports to `127.0.0.1` only.
</Warning>

## Enable

<Steps>
<Step title="Open Settings → Router">
Open the app, then **Settings → Router**. The switch lives in the **Computer** card (`aria-label` `Use local Docker VM`, `role="switch"`).

Off copy: `Shell, files and computer use run on Grok Bot's remote computer.`

On copy: `status.detail` from `getBoxRuntime`, or `Shell, files and computer use run in a Docker container on this Mac.`
</Step>
<Step title="Turn on Use local Docker VM">
The renderer sets local UI state to `"local-docker"` and `busy: true`, then:

```ts
window.desktop.agent.setBoxRuntime("local-docker")
```

Preload forwards `{ mode }`. Main rejects any other string with `Unknown box runtime.`
</Step>
<Step title="Wait for persist, container, and health">
`setBoxRuntime` writes `boxRuntime: "local-docker"` first, then `startLocalDockerBox`. That path:

1. Ensures a 32+ character hex token in `local-docker-vm.json` next to `settings.json`.
2. Stages content-addressed `host-main.cjs` and `box-exec-daemon/main.cjs` under `local-docker-runtime/<host-sha256>-<daemon-sha256>/`.
3. Requires `docker info`.
4. Creates or starts `grok-bot-local-vm` with owner label `com.grok-bot.local-vm=1` and schema version `6`.
5. Polls `http://127.0.0.1:1340/health` with `Authorization: Bearer <token>` every 1s, 2s probe timeout, 180s overall.

Ready status: `Local Docker VM is ready.`
</Step>
<Step title="Confirm coordinator reconnect">
On success, main calls `boxRecovery.restartCoordinator` and returns `{ mode, status }`. The switch stays on. The coordinator then connects through the settings-routed host connector to `http://127.0.0.1:1340` instead of the remote broker.
</Step>
</Steps>

<RequestExample>
```json title="setBoxRuntime enable"
{ "mode": "local-docker" }
```
</RequestExample>

<ResponseExample>
```json title="setBoxRuntime success"
{
  "mode": "local-docker",
  "status": {
    "available": true,
    "running": true,
    "ready": true,
    "containerName": "grok-bot-local-vm",
    "image": "public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest",
    "detail": "Local Docker VM is ready."
  }
}
```
</ResponseExample>

## Disable

Turn the same switch off. Main writes `boxRuntime: "remote"`, then `stopLocalDockerBox`:

- Missing or already stopped container: no-op.
- Owned running container: `docker stop grok-bot-local-vm`.
- Running **unowned** container: throws `Refusing to stop unowned container grok-bot-local-vm.`, reverts `boxRuntime` to `"local-docker"`, and does not restart the coordinator.

Stop does **not** `docker rm` the container or delete volumes `grok-bot-local-vm-workspace` and `grok-bot-local-vm-data`. Success still restarts the coordinator so later `connect()` uses the remote host connector.

## Persist and RPC

`SandBoxRuntime` is `"remote" | "local-docker"`. `DEFAULT_SAND_BOX_RUNTIME` is `"remote"`. Invalid `boxRuntime` values in `settings.json` are dropped; load then returns the default.

<ParamField body="boxRuntime" type="SandBoxRuntime" default="remote">
Stored on `SandStoredSettings` version `1`. Written atomically via a pid-scoped temp file next to `settings.json`.
</ParamField>

<ParamField body="mode" type="string" required>
`setBoxRuntime` argument. Must pass `isSandBoxRuntime`.
</ParamField>

<ResponseField name="mode" type="SandBoxRuntime">
Persisted runtime after a successful start or stop.
</ResponseField>

<ResponseField name="status" type="LocalDockerStatus">
`available`, `running`, `ready`, `containerName`, `image`, `detail` from `getLocalDockerStatus`.
</ResponseField>

`getBoxRuntime` returns the stored mode plus a live Docker inspect/health snapshot. It does not start the VM.

```mermaid
sequenceDiagram
  participant UI as Settings Router
  participant Edge as setBoxRuntime
  participant Store as settings.json
  participant VM as grok-bot-local-vm
  participant Health as 127.0.0.1:1340/health
  participant Coord as Coordinator

  UI->>Edge: mode local-docker
  Edge->>Store: boxRuntime = local-docker
  Edge->>VM: docker run or start
  loop every 1s up to 180s
    Edge->>Health: GET /health Bearer token
    alt response.ok
      Edge->>Coord: restartCoordinator
      Edge-->>UI: mode + LocalDockerStatus
    else container exited
      Edge->>Store: boxRuntime = remote
      Edge-->>UI: throw
    end
  end
```

## What the container does

Image: `public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest`.

| Fact | Value |
| --- | --- |
| Name | `grok-bot-local-vm` |
| Gateway | `http://127.0.0.1:1340` |
| Health | `GET /health`, Bearer `SAND_GATEWAY_TOKEN` |
| Owner label | `com.grok-bot.local-vm=1` |
| Schema label | `com.grok-bot.local-vm.schema-version=6` |
| Restart policy | `unless-stopped` |
| Auto-update | `SAND_BOX_AUTO_UPDATE=0` |
| Gateway bind in-box | `SAND_GATEWAY_BIND_HOST=0.0.0.0`, `SAND_HOST_PORT=1340` |

Host mounts are read-only content-addressed files, not the stock image copies. Optional read-only binds of `~/.codex` → `/root/.codex` and `~/.claude` → `/root/.claude` apply when those directories exist. Published ports are loopback-only.

After a successful toggle, coordinator `connect()` may still call remote `issueInferenceCredential` with a 3s timeout. If a token arrives, the connector can replace the container so it mounts `local-docker-credential/inference.json`. Toggle-on `startLocalDockerBox` itself does not wait for that credential.

Computer recreate while local: `docker restart grok-bot-local-vm`, then wait for health (`started-untrackable`). Force recreate: `docker rm --force`, then create again.

## Status strings

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

## Failure and rollback

Optimistic UI flips the switch immediately. If `setBoxRuntime` throws, the renderer restores the previous mode and shows `error.message`.

Main rollback: failed `"local-docker"` → persist `"remote"`; failed `"remote"` → persist `"local-docker"`. Coordinator restart runs only after start/stop succeeds.

| Error | Typical cause |
| --- | --- |
| `Unknown box runtime.` | `mode` is not `"remote"` or `"local-docker"`. |
| `Local Docker VM is selected, but Docker is unavailable: …` | Daemon not running or `docker` missing. |
| `Local Docker VM cannot use grok-bot-local-vm: an unowned container already has that name.` | Name collision; connector will not take over. |
| `Local Docker VM container uses unexpected image …` | Existing owned container image ≠ pinned image. |
| `The reconstructed runtime is unavailable … refusing to start a stock local VM.` | `host-main.cjs` / `box-exec-daemon/main.cjs` not next to the compiled main. |
| `Could not create` / `Could not start` / `Could not replace` | `docker run` / `start` / `rm --force` non-zero. |
| `Local Docker VM stopped before its gateway became ready.` | Container exited during the 180s wait; last 80 log lines attached. |
| `Local Docker VM did not expose its gateway within three minutes.` | Process still running, `/health` never OK. |
| `Refusing to stop unowned container grok-bot-local-vm.` | Disable path while a foreign container is running. |

Replace (rm + recreate) happens when schema ≠ `6`, host SHA-256 label mismatches the staged `host-main.cjs`, or an inference credential is present but the container lacks `com.grok-bot.local-vm.inference-credential=1`.

<AccordionGroup>
<Accordion title="Inspect the VM from a shell">
```sh
docker inspect grok-bot-local-vm --format '{{index .Config.Labels "com.grok-bot.local-vm"}} {{index .Config.Labels "com.grok-bot.local-vm.schema-version"}}'
docker logs --tail 80 grok-bot-local-vm
```

Health from the host needs the token in `local-docker-vm.json` beside `settings.json` (`schemaVersion: 1`).
</Accordion>
</AccordionGroup>

## Next

<CardGroup>
<Card title="Box runtime" href="/box-runtime">
`SandBoxRuntime` values, default `remote`, and coordinator restart on switch.
</Card>
<Card title="Local Docker connector" href="/docker-sandbox-reference">
Image, labels, schema 6, mounts, and loopback gateway.
</Card>
<Card title="Router and sandbox failures" href="/router-failures">
Unowned `grok-bot-local-vm` and other router/sandbox errors.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
`getBoxRuntime` / `setBoxRuntime` on `window.desktop.agent`.
</Card>
</CardGroup>

---

## 10. Route Grok Bot plugin tools

> listRoutedMcpTools and executeRoutedMcpTool across Claude Code MCP bridge versus Codex/OpenRouter direct tool execution.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/10-route-grok-bot-plugin-tools.md
- Generated: 2026-08-24T05:54:15.074Z

### Source Files

- `source/node-agent-coordinator/routed-mcp-bridge.ts`
- `source/node-agent-coordinator/inference-router.ts`
- `source/shared/rpc/coordinator.ts`
- `source/electron-main/coordinator/coordinator-executors.ts`
- `source/host/extensions/mcp/mcp-service.ts`
- `tests/backend-mcp-exec-json.test.mjs`

---
title: "Route Grok Bot plugin tools"
description: "listRoutedMcpTools and executeRoutedMcpTool across Claude Code MCP bridge versus Codex/OpenRouter direct tool execution."
---

When `inferenceProvider` is `claude-code`, `codex`, or `openrouter`, a non-Cursor `sendPrompt` is handled in the node-agent coordinator. That turn lists already-connected Grok Bot plugins with `listRoutedMcpTools` and runs model-selected tools with `executeRoutedMcpTool`. Cursor turns do not use this intercept: they keep the native host MCP executor.

<Note>
Routed plugin tools are the plugins and HTTP/SSE accounts already installed in Grok Bot. The model is instructed not to ask the user to reconnect those plugins or supply API keys for them.
</Note>

## Prerequisites

<Steps>
<Step title="Connect plugins in Grok Bot">
Install and authenticate plugins in the shipped Plugins UI. Routed providers reuse that catalog; they do not start a separate MCP config for Claude Code, Codex, or OpenRouter.
</Step>
<Step title="Select a routed provider">
Set Settings → Router to `claude-code`, `codex`, or `openrouter`. `cursor` (default) skips this routing path.
</Step>
<Step title="Satisfy provider login">
Claude Code must be installed and signed in. Codex needs a private `auth.json` under `CODEX_HOME` or `~/.codex`. OpenRouter needs `OPENROUTER_API_KEY` in the environment or Settings → Router secrets.
</Step>
</Steps>

## How a routed turn uses plugins

On `sendPrompt`, `createCoordinatorInferenceRouter` queues one turn per `agentId`, then:

1. Lists tools (or starts the Claude Code HTTP MCP bridge that lists them on `tools/list`).
2. Calls `runRoutedProviderText` with either `mcpServerUrl` (Claude Code) or `{ tools, executeTool }` (Codex and OpenRouter).
3. Forwards each tool call over desktop control RPC as `executeRoutedMcpTool`.
4. Closes the Claude Code bridge in `finally`.

Coordinator `dispatchRemote` sends those two methods through Electron control commands, not through the host gateway. Missing desktop wiring throws `Desktop MCP routing is unavailable.`

```mermaid
flowchart TB
  subgraph Renderer["Renderer"]
    Send["sendPrompt"]
  end
  subgraph Coordinator["node-agent-coordinator"]
    Router["createCoordinatorInferenceRouter"]
    Bridge["createRoutedMcpBridge\nhttp://127.0.0.1:&lt;port&gt;/mcp/&lt;uuid&gt;"]
    Direct["runRoutedProviderText\ntools + executeTool"]
  end
  subgraph Desktop["electron-main control RPC"]
    Exec["listRoutedMcpTools\nexecuteRoutedMcpTool"]
    Mgr["createSandDesktopMcpManager"]
  end
  subgraph Backend["Cursor dashboard MCP"]
    List["listSandMcpTools"]
    Call["executeSandMcpTool"]
  end
  Send --> Router
  Router -->|"provider === claude-code"| Bridge
  Router -->|"codex or openrouter"| Direct
  Bridge -->|"JSON-RPC tools/list and tools/call"| Exec
  Direct --> Exec
  Exec --> Mgr
  Mgr --> List
  Mgr --> Call
```

Coordinator RPC contract:

| Method | Args | Reply |
| --- | --- | --- |
| `listRoutedMcpTools` | none | array |
| `executeRoutedMcpTool` | object | record |

Telemetry domain for both methods is `plugins`.

## Provider execution modes

| Provider | Tool surface | Max tool steps | Executor |
| --- | --- | --- | --- |
| `claude-code` | Loopback MCP HTTP server `grok_bot_plugins` | `maxTurns` 8 when the bridge URL is set, else 1 | Claude Agent SDK `query` with `tools: ["mcp__grok_bot_plugins__*"]`, `strictMcpConfig: true`, `permissionMode: "default"`, `persistSession: false` |
| `codex` | Direct function tools on `https://chatgpt.com/backend-api/codex/responses` | `maxSteps` 8 when tools exist, else 1 | `streamCodexDirectResponses` with `tool_choice: "auto"` and `parallel_tool_calls: true` |
| `openrouter` | AI SDK `tool()` set | `maxSteps` 8 when tools exist, else 1 | `streamText` against `https://openrouter.ai/api/v1`, model `SAND_OPENROUTER_MODEL` or `openai/gpt-5.2` |
| `cursor` | Native host MCP (`getTools` / `SandMcpExecutor`) | n/a on this path | Not intercepted |

<Tabs>
<Tab title="Claude Code MCP bridge">
The coordinator binds `createRoutedMcpBridge` only for `claude-code`. The server listens on `127.0.0.1` with an ephemeral port and a UUID path secret.

- Accepts `POST /mcp/<secret>` only. Other methods/paths return 404.
- Body limit is 1,048,576 bytes (413 if exceeded). Invalid JSON returns 400.
- `initialize` reports `protocolVersion: "2025-03-26"`, `serverInfo.name: "grok-bot-plugins"`, `capabilities.tools.listChanged: false`.
- `tools/list` calls `listRoutedMcpTools` and caches tools by `name`.
- `tools/call` looks up that cache and calls `executeRoutedMcpTool` with the selected tool, `arguments`, a new `toolCallId`, and the current `agentId`.
- Unknown names return MCP `{ isError: true, content: [{ type: "text", text: "Unknown Grok Bot plugin tool: …" }] }`.

Read-only annotations are a name/description heuristic (`read`, `search`, `list`, … without `send`, `create`, `delete`, …): `readOnlyHint` / `idempotentHint` versus `destructiveHint` / `openWorldHint`.
</Tab>
<Tab title="Codex and OpenRouter direct tools">
For `codex` and `openrouter`, the coordinator loads `listRoutedMcpTools` once per turn and passes the array into `runRoutedProviderText`.

Each definition needs a non-empty `name` and `inputSchema` (or `parameters`). The model calls the Grok Bot `name`. Execution always goes back through `executeRoutedMcpTool` with:

- `providerIdentifier`
- `name`
- `toolName`
- `args`
- `toolCallId`
- `agentId`

Codex continues the Responses stream with `function_call_output` using the exact `call_id`. Unknown names become `{ isError: true, error: "Unknown Grok Bot tool: …" }`. Missing `executeTool` when Codex requests a tool throws `Codex requested a tool but Grok Bot did not provide an executor.`
</Tab>
</Tabs>

Shared system text (`GROK_ROUTER_SYSTEM_PROMPT`) states the process is Grok Bot, not Codex CLI or Claude Code, and that supplied tools are already-connected plugins.

## RPC and wire shapes

### `listRoutedMcpTools`

No arguments. Desktop implementation is `listRoutedTools()` on `createSandDesktopMcpManager`: a warmed snapshot from `createMcpToolsDiscovery().getTools()`, or a live discovery if the snapshot is still empty. Discovery starts at manager construction; failures report desktop-edge `routed-tools-warm`.

<ResponseField name="name" type="string" required>
Grok Bot tool id the model calls (also the Claude MCP tool name).
</ResponseField>
<ResponseField name="providerIdentifier" type="string" required>
MCP server / account identifier (HTTP backend uses this as `serverIdentifier`).
</ResponseField>
<ResponseField name="toolName" type="string" required>
Provider-native tool name (used for disable checks and custom-instruction lookup).
</ResponseField>
<ResponseField name="description" type="string">
Optional. The Claude bridge defaults to `` `${toolName} via ${providerIdentifier}` ``.
</ResponseField>
<ResponseField name="inputSchema" type="object">
JSON Schema. The Claude bridge falls back to `{ type: "object", additionalProperties: true }`.
</ResponseField>

Host gateway listing (also used by `refreshMcp` with `routedAction: "list-tools"`) maps protobuf `inputSchema.toJson()` when present and merges `listConnectedBackendTools()` with discovery, first `name` wins.

### `executeRoutedMcpTool`

<ParamField body="providerIdentifier" type="string" required>
Server identifier of the installed plugin/account.
</ParamField>
<ParamField body="name" type="string" required>
Grok Bot tool id. HTTP backend execution sends this field as `toolName` to `executeSandMcpTool`.
</ParamField>
<ParamField body="toolName" type="string" required>
Native tool name. Disable lists in `settings.json` `mcpDisabledToolsByServerId` match this value.
</ParamField>
<ParamField body="args" type="object" required>
JSON arguments. Nested protobuf-like values with `toJson()` are flattened before backend serialization.
</ParamField>
<ParamField body="toolCallId" type="string" required>
Call id. The Claude bridge generates a UUID when Claude omits one.
</ParamField>
<ParamField body="agentId" type="string">
Current agent. Passed as MCP exec audit identity; backend `agentId` defaults to `""` if omitted.
</ParamField>

<RequestExample>
```json title="executeRoutedMcpTool"
{
  "providerIdentifier": "user-Gmail",
  "name": "gmail_search",
  "toolName": "search_threads",
  "args": { "query": "in:inbox", "pageSize": 1 },
  "toolCallId": "call-1",
  "agentId": "agent-1"
}
```
</RequestExample>

Backend `executeSandMcpTool` requires a protobuf `Struct`. Routed JSON is converted with `Struct.fromJson` at `createDashboardSandBackendMcpExec` so Connect never receives a plain object. List timeout is 60s; execute timeout is 180s (`3 * MCP_SDK_REQUEST_TIMEOUT_MS`). Deadline exceeded returns a timeout error that warns the connector may already have applied the call.

MCP results from the Claude bridge unwrap `{ result: { case: "success", value } }` into MCP `content` (`text` / `image`) plus optional `structuredContent`. Non-success cases become `{ isError: true, content: [{ type: "text", text }] }`.

## Where tools actually run

Desktop discovery is the path the coordinator uses:

| Transport | List | Execute from routed providers |
| --- | --- | --- |
| HTTP / SSE | Cursor `listSandMcpTools` | Cursor `executeSandMcpTool` |
| Box stdio | Desktop `boxMcpExec.listTools` returns servers with `tools: []` | Desktop stub: `MCP tools run on Grok Bot's computer, not the desktop app (tool "…").` |

HTTP/SSE plugins are the tools routed providers can call. Stdio servers still run on the box for Cursor-native turns, not through this desktop RPC.

Disabled tools (`mcpDisabledToolsByServerId`) are omitted from lists. Execute of a disabled `toolName` returns `Tool "<toolName>" is disabled for "<server>".` Successful results may prepend the server's custom instruction note from `mcpCustomInstructionsByServerId` (fallback: legacy `mcpCustomInstructions` by display name).

Tool cache TTL is 24 hours (`MCP_TOOLS_CACHE_TTL_MS`). Full discovery deadline is 120 seconds.

The host process also registers gateway commands `listRoutedMcpTools` / `executeRoutedMcpTool` and `refreshMcp` aliases `list-tools` / `execute-tool`. Those host helpers are not what the inference router calls for a routed turn.

## Failure modes

| Condition | Behavior |
| --- | --- |
| Desktop MCP adapters unset | `Desktop MCP routing is unavailable.` |
| Claude Code CLI missing | `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.` |
| Codex `auth.json` not a private regular file | `Codex login credentials must be a private direct regular file.` |
| Missing OpenRouter key | `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.` |
| Backend list failure | `Backend MCP tool discovery failed: …` |
| Backend execute timeout | Timeout message after 180s; retry only if the connector call is safe to repeat |
| HTTP server unknown here | `MCP server "<id>" is not available here. HTTP/SSE servers execute on the backend and stdio servers run on Grok Bot's computer; …` |
| Routed `sendPrompt` throws | Coordinator appends `Router error: …` as an assistant transcript row |

<Warning>
`cursor` does not list or execute plugins through this coordinator path. Switching Router away from Cursor is what attaches Grok Bot plugins to Claude Code, Codex, or OpenRouter.
</Warning>

## Related pages

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, transcript store, and the `sendPrompt` intercept that owns this tool routing.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Persist `inferenceProvider` and satisfy Cursor, Claude Code, Codex, or OpenRouter login.
</Card>
<Card title="Provider clients" href="/provider-clients">
Claude Agent SDK, Codex Responses, and OpenRouter OpenAI-compatible clients used after tools are listed.
</Card>
<Card title="Settings schema" href="/settings-schema">
`mcpCustomInstructions*`, `mcpDisabledToolsByServerId`, and `inferenceProvider`.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
Control-channel shape used by coordinator `listRoutedMcpTools` / `executeRoutedMcpTool`.
</Card>
<Card title="Router and sandbox failures" href="/router-failures">
Unknown provider, missing Claude Code, private Codex `auth.json`, and OpenRouter key errors.
</Card>
</CardGroup>

---

## 11. Bootstrap the pinned runtime

> npm run bootstrap order: GROK_BOT_018_APP, cached app, LFS DMG, then public URL; verify DMG and app.asar SHA-256; hydrate src/app/dist.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/11-bootstrap-the-pinned-runtime.md
- Generated: 2026-08-24T05:54:05.953Z

### Source Files

- `scripts/bootstrap-runtime.mjs`
- `scripts/lib/runtime.mjs`
- `scripts/lib/config.mjs`
- `tests/publication-bootstrap.test.mjs`
- `research-archives/original/0.18.0/artifacts.json`
- `research-archives/README.md`

---
title: "Bootstrap the pinned runtime"
description: "npm run bootstrap order: GROK_BOT_018_APP, cached app, LFS DMG, then public URL; verify DMG and app.asar SHA-256; hydrate src/app/dist."
---

`npm run bootstrap` runs `node scripts/bootstrap-runtime.mjs`. That script materializes a checksum-pinned Grok Bot **0.18.0** macOS arm64 Electron app into `.cache/runtime/Grok Bot.app`, then extracts `Contents/Resources/app.asar` into ignored `src/app/dist` after verifying the ASAR SHA-256. Later packaging (`npm run package`) copies that hydrated `src/app` tree and refuses to start without the cached 0.18.0 runtime.

<Note>
Bootstrap is a local, content-addressed input step. It does not choose an inference provider, start Docker, or write `dist/Grok Bot 0.18 Reconstructed.app`.
</Note>

## Prerequisites

| Requirement | Why bootstrap needs it |
| --- | --- |
| macOS Apple Silicon | The pinned installer is `darwin-arm64`. Extraction uses `/usr/bin/hdiutil`, `/usr/bin/ditto`, and `/usr/bin/plutil`. |
| Node `>=26.5.0 <27` | `package.json` `engines.node` |
| Git LFS | Preserved DMG at `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` |
| `npm ci` | `@electron/asar` is used to unpack `app.asar` |

```sh
git lfs install
git lfs pull
npm ci
npm run bootstrap
```

Optional: skip the DMG path by pointing `GROK_BOT_018_APP` at an already installed `Grok Bot.app` whose `CFBundleShortVersionString` is `0.18.0`.

## Resolution order

The script picks **one** runtime source, then always hydrates `src/app/dist` from that app's `app.asar`.

```mermaid
flowchart TD
  start([npm run bootstrap]) --> env{"GROK_BOT_018_APP set?"}
  env -->|yes| copyEnv["ditto into .cache/runtime/Grok Bot.app"]
  env -->|no| cachedApp{".cache/runtime/Grok Bot.app exists?"}
  cachedApp -->|yes| validateCached["validateRuntimeApp"]
  cachedApp -->|no| dmg["downloadDmg"]
  dmg --> cachedDmg{".cache/downloads/Grok_Bot_0.18.0.dmg matches pin?"}
  cachedDmg -->|yes| attach
  cachedDmg -->|no| lfs{"LFS archive exists?"}
  lfs -->|yes| verifyLfs["SHA-256 then copy to cache"]
  lfs -->|no| fetchUrl["GET dmgUrl → .partial → rename"]
  verifyLfs --> attach["hdiutil attach Grok Bot.app"]
  fetchUrl --> attach
  attach --> copyDmg["ditto into .cache/runtime/Grok Bot.app"]
  copyEnv --> hydrate
  validateCached --> hydrate
  copyDmg --> hydrate["hydrateSourcePayloadFromRuntime"]
  hydrate --> asarPin{"app.asar SHA-256 == upstreamAsarSha256?"}
  asarPin -->|yes| dest["replace src/app/dist"]
  asarPin -->|no| failAsar["throw checksum mismatch"]
```

### 1. `GROK_BOT_018_APP`

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute or relative path to a `Grok Bot.app` bundle. The value is trimmed; empty/whitespace is ignored.
</ParamField>

When set, bootstrap never downloads a DMG. It validates the path, deletes any previous `.cache/runtime/Grok Bot.app`, and copies the bundle with `/usr/bin/ditto`.

### 2. Cached app

If the env var is unset and `.cache/runtime/Grok Bot.app` already exists, bootstrap only re-validates that copy.

### 3. Cached DMG, then LFS, then public URL

If no cached app exists, `downloadDmg()` fills `.cache/downloads/Grok_Bot_0.18.0.dmg`:

1. Reuse the cached DMG when its SHA-256 already matches the pin. A mismatched cache file is deleted.
2. If `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` exists, hash it. On match, copy it into the cache. On mismatch, throw and tell you to `git lfs pull`.
3. Otherwise `fetch()` the public URL with `redirect: "follow"`, write `.cache/downloads/Grok_Bot_0.18.0.dmg.partial` at mode `0o600`, verify SHA-256, then rename into the cache. A failed HTTP response or digest mismatch deletes the partial file.

The Windows `setup.exe` in the same archive is **not** a bootstrap input.

### 4. Extract `Grok Bot.app` from the DMG

`hdiutil attach -readonly -nobrowse -mountpoint` mounts the cached DMG under a temp directory named `grok-bot-018-mount-*`. Bootstrap copies `<mount>/Grok Bot.app` into the runtime cache, then detaches and removes the mountpoint.

## Pinned identities

Constants live in `scripts/lib/config.mjs` and must match `research-archives/original/0.18.0/artifacts.json` (`schemaVersion` `1`) for the macOS artifact.

| Pin | Value |
| --- | --- |
| `upstreamVersion` | `0.18.0` |
| `dmgUrl` | `https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg` |
| `dmgSha256` | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| `upstreamAsarSha256` | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |
| macOS archive bytes | `155793020` |

Independent check of the LFS files (not performed by bootstrap itself):

```sh
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
```

`tests/research-archives.test.mjs` also requires each installer byte length to equal `artifacts.json` `bytes`. A Git LFS pointer file fails that size check.

## Runtime validation

`validateRuntimeApp()` runs on every accepted app (env copy, cache hit, or DMG extract):

| Check | Path / tool |
| --- | --- |
| Version | `plutil -extract CFBundleShortVersionString raw Contents/Info.plist` must equal `0.18.0` |
| Executable | `Contents/MacOS/Grok Bot` must be a file |
| Unpacked natives | `Contents/Resources/app.asar.unpacked` must be a directory |

Failure messages:

- `Expected Grok Bot 0.18.0, got <version> at <appPath>`
- `Incomplete Grok Bot runtime at <appPath>`

Packaging later calls `resolveRuntimeApp()`. If `GROK_BOT_018_APP` is unset and the cache is missing, it throws `Missing 0.18.0 runtime. Run \`npm run bootstrap\` first.`

## Hydrate `src/app/dist`

`hydrateSourcePayloadFromRuntime()` hashes `Contents/Resources/app.asar` as raw bytes. The digest must equal `upstreamAsarSha256`. It then:

1. Extracts the archive under `.cache/source-payloads/grok-bot-018-*`.
2. Requires these files to exist as regular files:
   - `dist/electron-main/main.cjs`
   - `dist/host/host-main.cjs`
   - `dist/renderer/index.html`
3. Deletes `src/app/dist` if present and copies only the archive's `dist/` tree there (`dereference: false`, timestamps preserved).
4. Deletes the temporary extract directory.

Checked-in `src/app/package.json` is not replaced. `/src/app/dist/` and `/.cache/` are gitignored; do not add the hydrated tree.

<Warning>
Hydration is the shipped renderer and compiled payload, not `frontend/`. Packaged UI stays this checksum-pinned `dist/renderer` unless a later packaging override is set.
</Warning>

`tests/publication-bootstrap.test.mjs` asserts a matching SHA-256 hydrates `dist/renderer/index.html` and that a wrong `expectedSha256` rejects with `/checksum mismatch/`.

## Paths and outputs

:::files
repo/
├── research-archives/original/0.18.0/
│   ├── artifacts.json
│   ├── SHA256SUMS
│   └── macos-arm64/Grok_Bot_0.18.0.dmg   # Git LFS
├── src/app/
│   ├── package.json                      # checked in
│   └── dist/                             # hydrated, gitignored
└── .cache/
    ├── downloads/Grok_Bot_0.18.0.dmg
    ├── runtime/Grok Bot.app
    └── source-payloads/grok-bot-018-*/
:::

<RequestExample>
```sh
npm run bootstrap
```
</RequestExample>

<ResponseExample>
```text
Using archived release .../research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg
Runtime ready: .../.cache/runtime/Grok Bot.app
Checksum-pinned source payload ready: .../src/app/dist (6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58)
The checksum-pinned app supplies only the Electron shell, ABI-matched native dependencies, and explicitly documented build fallbacks.
```
</ResponseExample>

When the LFS file is absent and the cache is empty, the first log line is `Downloading https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg` instead of `Using archived release`.

## What packaging consumes next

`scripts/lib/build-asar.mjs` copies the whole `src/app` directory (including hydrated `dist/`) into `.build/app`, then overlays ABI-matched `deps` and `native` trees from the cached app's `app.asar.unpacked`. The bootstrap console line is literal: the cached app is the Electron shell, native ABI, and documented fallbacks—not reconstructed TypeScript under `source/`.

## Failures bootstrap throws

| Symptom | Typical cause | What to do |
| --- | --- | --- |
| `Archived DMG checksum mismatch ... Run git lfs pull` | Pointer-sized or corrupt LFS object | `git lfs pull`, then `shasum -a 256 -c SHA256SUMS` |
| `DMG checksum mismatch` after download | Truncated or substituted URL payload | Delete `.cache/downloads/Grok_Bot_0.18.0.dmg*` and retry |
| `Download failed: HTTP <status>` | Public URL unavailable | Use LFS or `GROK_BOT_018_APP` |
| `Expected Grok Bot 0.18.0, got …` | Wrong app behind `GROK_BOT_018_APP` or stale cache | Point at 0.18.0 or remove `.cache/runtime` |
| `Incomplete Grok Bot runtime` | Missing `Contents/MacOS/Grok Bot` or `app.asar.unpacked` | Re-extract from the pinned DMG |
| `Upstream app.asar checksum mismatch` | ASAR is not the 0.18.0 pin | Do not bypass the hash; replace the runtime |
| `Upstream app.asar is missing dist/…` | Archive contents incomplete | Fail closed; do not hand-copy files into `src/app/dist` |

Full failure catalog: [Bootstrap failures](/bootstrap-failures).

## Next

<CardGroup>
  <Card title="Package the macOS app" href="/package-macos-app">
    Compile reconstructed runtimes, replace app.asar, ad-hoc sign, and verify.
  </Card>
  <Card title="Bootstrap failures" href="/bootstrap-failures">
    LFS pointers, checksum mismatches, version errors, and missing ASAR files.
  </Card>
  <Card title="Preserve original installers" href="/preserve-original-installers">
    artifacts.json schemaVersion 1, LFS DMG/EXE identities, SHA256SUMS.
  </Card>
  <Card title="Reconstruction boundary" href="/reconstruction-boundary">
    Ignored src/app/dist hydration versus compiled source/ runtimes.
  </Card>
</CardGroup>

---

## 12. Package the macOS app

> npm run package compiles runtimes, patches Settings Router, replaces app.asar, sets reconstructed identity, ad-hoc signs, and verifies.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/12-package-the-macos-app.md
- Generated: 2026-08-24T05:54:33.562Z

### Source Files

- `scripts/package-macos.mjs`
- `scripts/clean-build.mjs`
- `scripts/lib/codesign.mjs`
- `scripts/lib/macos-package-verification.mjs`
- `scripts/lib/build-asar.mjs`
- `scripts/lib/router-renderer-patch.mjs`
- `tests/publication-packaging.test.mjs`

---
title: "Package the macOS app"
description: "npm run package compiles runtimes, patches Settings Router, replaces app.asar, sets reconstructed identity, ad-hoc signs, and verifies."
---

`npm run package` runs `npm run check`, then `node scripts/package-macos.mjs`. That script aborts unless `process.platform === "darwin"`, builds a **fidelity-hybrid** ASAR (`buildKind: "fidelity-hybrid-reconstruction"`), copies the checksum-pinned 0.18 Electron shell with `/usr/bin/ditto`, replaces `Contents/Resources/app.asar` and `app.asar.unpacked`, rewrites reconstructed identity in `Info.plist`, ad-hoc signs with `codesign --sign -`, and verifies the result.

Default output:

```text
dist/Grok Bot 0.18 Reconstructed.app
```

The official cached `Grok Bot.app` is **reference-only**. Packaging copies its ABI-matched shell and helpers, then must **not** keep the official signed `Contents/MacOS/Grok Bot` identity or the official `app.asar` SHA-256.

<Warning>
Packaging is macOS Apple Silicon only. On any other platform the script throws: `The reconstructed macOS application can only be packaged on macOS.`
</Warning>

## Prerequisites

<Steps>
<Step title="Install the toolchain">
Apple Silicon macOS, Node `>=26.5.0 <27`, Xcode Command Line Tools (`/usr/bin/codesign`, `/usr/bin/ditto`, `/usr/bin/plutil`, `/usr/bin/xattr`), and Git LFS.
</Step>
<Step title="Hydrate the pinned 0.18 runtime">
Run `npm run bootstrap` so the cached app, official `app.asar`, and ignored `src/app/dist` renderer exist and match the pinned SHA-256 identities.
</Step>
<Step title="Install and typecheck">
`npm ci` then `npm run check` (the package script already runs check first: frontend typecheck, `source/` typecheck, and `tests/*.test.mjs`).
</Step>
</Steps>

## Command

```sh
npm run package
```

Equivalent:

```sh
npm run check && node scripts/package-macos.mjs
```

Success log (counts come from `runtime-deps-manifest.json` and the unpacked runtime snapshot):

```text
Packaged application: dist/Grok Bot 0.18 Reconstructed.app (<n> native manifest entries, <m> unpacked runtime files)
```

Override the `.app` basename with `GROK_BOT_OUTPUT_APP_NAME` (the value is passed through `path.basename`). Leave `CFBundleName` / `CFBundleExecutable` as `Grok Bot`: Electron derives nested helper names from that string, and this fork reuses the ABI-matched 0.18 runtime.

<ParamField body="GROK_BOT_OUTPUT_APP_NAME" type="string">
Optional. Basename of the bundle under `dist/`. Default: `Grok Bot 0.18 Reconstructed.app`.
</ParamField>

<ParamField body="GROK_BOT_HOST_BINDINGS_MANIFEST" type="string">
Optional. Schema version 1 host production-binding manifest. Unbound slots keep host in `artifact-fallback` mode; no inferred native fallback.
</ParamField>

<ParamField body="GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST" type="string">
Optional override. If unset and `manifests/reconstruction/electron-main-production-bindings-manifest.json` exists, that path is used. Unbound slots keep Electron main in `artifact-fallback`.
</ParamField>

<ParamField body="GROK_BOT_RENDERER_SOURCE" type="string">
Optional repo-relative directory with `index.html`. Replaces staged `dist/renderer` during ASAR staging. Default packaging does **not** use `frontend/` Vite output; it keeps the checksum-pinned shipped renderer plus the Settings Router patch.
</ParamField>

## Pipeline

```mermaid
flowchart TB
  subgraph gate [Gate]
    CHK["npm run check"]
    DAR["darwin only"]
  end
  subgraph compile [Clean-source compile]
    SRC["source/*.ts"]
    FID[".build/fidelity-clean-runtime"]
    ACT["host / electron-main activation"]
  end
  subgraph renderer [Renderer authority]
    ART["src/app/dist/renderer"]
    PROV["dist/renderer-artifact-provenance.json"]
    PATCH["applyOriginalRendererRouterPatch"]
    EXT["dist/renderer-router-extension.json"]
  end
  subgraph shell [Official 0.18 shell]
    RT["cached Grok Bot.app"]
    DITTO["ditto + xattr -cr"]
  end
  subgraph identity [Reconstructed identity]
    ASAR["replace app.asar + unpacked"]
    PLIST["com.anysphere.sand.reconstructed"]
    SIGN["codesign --sign -"]
    VER["verify shell, unpacked runtime, signature"]
  end
  CHK --> DAR --> SRC --> FID --> ACT
  ART --> PROV
  FID --> PATCH
  ART --> PATCH --> EXT
  RT --> DITTO --> ASAR --> PLIST --> SIGN --> VER
  EXT --> ASAR
  ACT --> ASAR
```

### 1. Compile reconstructed runtimes

`buildFidelityReconstructedAsar()` stages the bootstrapped app tree, then overlays a fidelity clean distribution:

| Runtime | Packaged path | Default mode |
| --- | --- | --- |
| electron-main | `dist/electron-main/main.cjs` | `artifact-fallback` unless production bindings validate, then `clean-source` |
| host | `dist/host/host-main.cjs` | same activation rule |
| primary / dev-controls / webview / VNC preloads | `dist/electron-preload/*.cjs` | `clean-source` |
| node-agent-coordinator | `dist/node-agent-coordinator/main.cjs` | `clean-source` |
| host workers | agent-store, transcript-mirror, box-store-vacuum, search-index | `clean-source` |
| box-exec-daemon / local-exec-daemon | `dist/box-exec-daemon/main.cjs`, `dist/local-exec-daemon/main.cjs` | `clean-source` |
| renderer | `dist/renderer` | `checksum-pinned-artifact-runtime` |
| electron-runtime-dependencies | `dist/deps` | `artifact-runtime` (copied from 0.18 unpacked) |
| node-runtime-dependencies | `dist/node-deps` | `generated-runtime` (local Node ABI tree-sitter rebuild) |
| native-runtime-tools | `dist/native` | `artifact-runtime` |
| electron-shell | `Contents/Frameworks/Electron Framework.framework` | `artifact-runtime` |

`dist/deps/node_modules/node-gyp-build` is a byte-exact sibling copy recorded as `resolutionClosure.mode: "byte-exact-sibling-package-copy"` in `dist/deps/runtime-deps-manifest.json`.

The composition audit is packed at `dist/runtime-composition-audit.json`. `dist/reconstruction-build.json` is rewritten to `schemaVersion: 2` after the audit is attached.

Clean-source executables carry a `// Deterministic clean-source` banner. Artifact-fallback Electron main is the immutable evidence bundle plus an explicit reconstructed transform (updater/telemetry guard and optional `GROK_BOT_RECONSTRUCTED_DEV` seams). When Electron main activates as `clean-source` with `reconstructedPackage: true`, the same updater guard is prepended to the bundled `main.cjs`.

### 2. Patch Settings Router into the shipped renderer

Default packaging does **not** swap in the Vite `frontend/` workspace. It copies the exact 0.18 renderer from `src/app/dist/renderer`, then `applyOriginalRendererRouterPatch` edits **exactly one** settings-registry chunk and **exactly one** settings-panel chunk.

Transforms:

| Transformation | Effect |
| --- | --- |
| `settings-registry` | Inserts `{id:"router",label:"Router",icon:"git-branch"}` between General and Usage |
| `router-panel` | Inserts `RRouterPanel` / `RBoxRuntime` and wires `x==="router"` |
| `usage-panel` | Routes Usage to `RRouterUsage` (Cursor usage UI remains when provider is `cursor`) |

Provider ids in the injected UI: `cursor`, `claude-code`, `codex`, `openrouter`. The panel talks to `window.desktop.agent.getInferenceRouter` / `setInferenceRouter`, `getBoxRuntime` / `setBoxRuntime`, and `window.desktop.secrets.list` / `upsert`. Missing or ambiguous minified anchors throw: `Original renderer <label> anchor is missing or ambiguous.` or `Expected one original Settings registry and panel chunk`.

The patch writes `dist/renderer-router-extension.json`:

- `schemaVersion`: `1`
- `mode`: `original-renderer-settings-extension`
- `features`: `settings-router-provider`, `settings-local-docker-vm`, `usage-current-provider`
- `chunks[]`: `role`, `path`, original and patched `{ bytes, sha256 }`

Renderer provenance `dist/renderer-artifact-provenance.json` stays bound to upstream `app.asar` SHA-256 `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58`. Byte checks that **allow** the Settings edit consume the extension record (`verifyChecksumPinnedRendererPackage`, used by `npm run package:diagnostic`).

### 3. Replace ASAR in the official shell

After `verifyOfficialMacReference` confirms the cached app’s `Contents/MacOS/Grok Bot` and `Contents/Resources/app.asar` hashes:

1. `ditto` the official app onto `dist/Grok Bot 0.18 Reconstructed.app` (after deleting any previous output).
2. `xattr -cr` the copy so Gatekeeper does not reject the new ad-hoc signature because of DMG quarantine.
3. Delete the copied `app.asar` and `app.asar.unpacked`.
4. Copy `.build/fidelity/app.asar` and `.build/fidelity/app.asar.unpacked`.

Unpacked trees that must stay byte-identical to the staged ASAR unpack: `dist/deps`, `dist/native`, `dist/node-deps`. `dist/host/extensions/codebase-telemetry/csnaps` must **not** be packaged.

### 4. Reconstructed identity

`plutil` updates `Contents/Info.plist`:

| Key | Value |
| --- | --- |
| `CFBundleIdentifier` | `com.anysphere.sand.reconstructed` |
| `CFBundleDisplayName` | `Grok Bot 0.18 Reconstructed` |
| `CFBundleURLTypes` | single Viewer entry, scheme `sand` only (`Grok Bot reconstructed auth callback`) |
| `ElectronAsarIntegrity` | **removed** (stale official integrity would reject the new archive) |
| `CFBundleName` / `CFBundleExecutable` | left as `Grok Bot` |

Inherited URL aliases such as `grokbot` are stripped. The official bundle on disk is never rewritten.

### 5. Ad-hoc sign and verify

`Contents/_CodeSignature` is removed, then:

```text
/usr/bin/codesign --force --deep --timestamp=none --sign - <app>
```

Identity `-` is ad-hoc. Stdio is non-interactive (`ignore` / `inherit` / `inherit`). If the first pass fails (nested framework signature still held after the copied runtime was in use), the script warns and retries **once**. Then:

```text
/usr/bin/codesign --verify --deep --strict <app>
```

`verifyReconstructedMacPackage` then requires:

- official shell and `app.asar` still match the pinned 0.18 hashes
- reconstructed Mach-O matches the official binary **after** signature exclusion, and is **not** a copy of the official signed shell
- reconstructed `app.asar` SHA-256 is **not** the official archive hash
- unpacked runtime files and `runtime-deps-manifest.json` are byte-identical to the staged unpack (`platform: darwin`, `arch: arm64`)

## Updater and telemetry seams

Packaged Electron main always prepends (idempotent):

```js
process.env.SAND_DISABLE_UPDATES ??= "1";
process.env.SAND_DISABLE_SENTRY ??= "1";
process.env.SAND_DISABLE_TELEMETRY ??= "1";
```

Explicit environment values still win. These defaults are a packaging-boundary guard, not a runtime settings toggle.

## Outputs

:::files
dist/
  Grok Bot 0.18 Reconstructed.app/
    Contents/
      Info.plist
      MacOS/Grok Bot
      Resources/
        app.asar
        app.asar.unpacked/
          dist/deps/
          dist/native/
          dist/node-deps/
.build/
  fidelity/
    app/
    app.asar
    app.asar.unpacked/
  fidelity-clean-runtime/
:::

Required ASAR entries (also checked by `npm run verify`):

`dist/electron-main/main.cjs`, preload set, `dist/node-agent-coordinator/main.cjs`, `dist/host/host-main.cjs` plus four host workers, `dist/local-exec-daemon/main.cjs`, `dist/renderer/index.html`, `dist/reconstruction-build.json`, `dist/runtime-composition-audit.json`, `package.json`.

Unpacked natives that must exist: `dist/deps/better-sqlite3/build/Release/better_sqlite3.node`, `dist/native/sand-webauthn-signer`, and generated tree-sitter Node bindings under `dist/node-deps`.

When host or Electron main remain `artifact-fallback`, recovered bundles `dist/recovered-source/host/host-main.cjs` and `dist/recovered-source/electron-main/main.cjs` must be present. Clean activation **removes** those fallbacks from the archive.

## Related commands

| Script | Role |
| --- | --- |
| `npm run bootstrap` | Pin DMG / `app.asar`, cache the shell, hydrate `src/app/dist` |
| `npm run check` | Typecheck + unit tests; always runs before package |
| `npm run package:diagnostic` | Same fidelity ASAR with renderer-extension-aware checksum verification; writes a uniquely named diagnostic `.app` |
| `npm run verify` | Inspect a packaged `.app` (default `dist/Grok Bot 0.18 Reconstructed.app`, or `--app <path>`): ASAR listing, composition audit, bundle id, `sand` URL, leftover `ElectronAsarIntegrity`, codesign |
| `npm run smoke` | Native e2e against the packaged app |

<Note>
`npm run package` does **not** invoke `scripts/verify.mjs`. It verifies official hashes, the reconstructed shell invariant, unpacked runtime bytes, and `codesign --verify`. `npm run verify` additionally walks ASAR membership and `reconstruction-build.json`. Its checksum-pinned renderer loop hashes packaged files against the **original** provenance inventory and does not apply `renderer-router-extension.json`; use `verifyChecksumPinnedRendererPackage` / `npm run package:diagnostic` for patch-aware renderer bytes.
</Note>

## Failure modes

| Symptom | Cause |
| --- | --- |
| `can only be packaged on macOS` | Non-darwin host |
| Official shell / `app.asar` hash mismatch | Bootstrap cache drifted; re-run bootstrap |
| Missing Settings registry/panel chunk | Shipped renderer anchors changed or `GROK_BOT_RENDERER_SOURCE` is not the 0.18 bundle |
| Ad-hoc sign fails twice | Nested framework still in use; quit the app and retry |
| Gatekeeper rejects launch | Quarantine xattrs not cleared (`xattr -cr` is required after `ditto`) |
| `Reconstructed package must not copy the official app.asar` | Replacement copy failed |
| Missing unpacked `dist/deps` / `dist/native` / `dist/node-deps` | Integrity unpack incomplete |
| Stale `ElectronAsarIntegrity` | `plutil -remove` did not run |

Do not weaken checksum, bundle-id, or signing gates to make a build pass.

## Next

<CardGroup>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
Hydrate the 0.18 DMG, shell, and `src/app/dist` before packaging.
</Card>
<Card title="Packaging failures" href="/packaging-failures">
Non-darwin abort, missing renderer anchors, codesign retry, quarantine, required ASAR entries.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
Post-package ASAR membership, evidence markers, and native e2e.
</Card>
<Card title="Reconstruction boundary" href="/reconstruction-boundary">
Clean-source vs checksum-pinned renderer vs `com.anysphere.sand.reconstructed`.
</Card>
<Card title="Work on the frontend reconstruction" href="/edit-frontend-workspace">
`frontend/` is a Vite design workspace; packaged UI stays the patched shipped renderer unless `GROK_BOT_RENDERER_SOURCE` overrides it.
</Card>
<Card title="Environment variables" href="/environment-variables">
`GROK_BOT_OUTPUT_APP_NAME`, binding manifests, `SAND_DISABLE_*`, renderer override.
</Card>
</CardGroup>

---

## 13. Work on the frontend reconstruction

> frontend/ is a Vite design workspace; packaged UI stays the patched shipped renderer unless GROK_BOT_RENDERER_SOURCE overrides it.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/13-work-on-the-frontend-reconstruction.md
- Generated: 2026-08-24T05:55:06.171Z

### Source Files

- `frontend/README.md`
- `frontend/vite.config.ts`
- `frontend/src/recovered/features/settings/overlay/router.ts`
- `frontend/src/recovered/features/settings/overlay/view.tsx`
- `scripts/recover-frontend.mjs`
- `scripts/lib/router-renderer-patch.mjs`

---
title: "Work on the frontend reconstruction"
description: "frontend/ is a Vite design workspace; packaged UI stays the patched shipped renderer unless GROK_BOT_RENDERER_SOURCE overrides it."
---

`frontend/` is a Vite 8.2.1 React/TypeScript workspace. `npm run package` does **not** ship that tree. The default macOS app keeps the checksum-pinned 0.18.0 renderer from hydrated `src/app/dist/renderer`, then applies an exact-once Settings Router extension. Set `GROK_BOT_RENDERER_SOURCE` only when you intentionally replace staged `dist/renderer` before that patch.

<Warning>
`frontend/README.md` says the Vite renderer is selected by the default macOS package. `scripts/package-macos.mjs` and `scripts/build.mjs` do the opposite: they call `buildFidelityReconstructedAsar()` and log `Renderer mode: checksum-pinned upstream 0.18.0 payload`.
</Warning>

## Two UIs, one repo

| Surface | Path | What it is | What ships |
| --- | --- | --- | --- |
| Vite workspace | `frontend/` | Readable reconstruction, typecheck, `frontend:build` | Nothing, unless you override |
| Hydrated shipped renderer | `src/app/dist/renderer` (gitignored) | Exact 0.18.0 payload from `npm run bootstrap` | Default packaged UI, after the Router patch |
| Expanded inspection copy | `recovered/frontend/` (gitignored) | `npm run frontend:recover` unminifies JS/CSS | Never packaged |
| Clean-source renderer builder | `scripts/renderer-production-build.mjs` | Separate Vite production graph rooted at `frontend/src/main.tsx` | Only when renderer mode is `clean-source`, not the default package |

```mermaid
flowchart TB
  subgraph workspace["frontend/ workspace"]
    SRC["frontend/src/main.tsx"]
    VITE["vite.config.ts base ./"]
    SHELL[".build/frontend-shell"]
    SRC --> VITE --> SHELL
  end
  subgraph hydrate["bootstrap hydration"]
    ASAR["pinned app.asar"]
    DIST["src/app/dist/renderer"]
    ASAR --> DIST
  end
  subgraph package["npm run package"]
    STAGE["staged dist/renderer"]
    PATCH["applyOriginalRendererRouterPatch"]
    EXT["dist/renderer-router-extension.json"]
    APP["Grok Bot 0.18 Reconstructed.app"]
    STAGE --> PATCH --> EXT --> APP
  end
  DIST --> STAGE
  ENV["GROK_BOT_RENDERER_SOURCE"] -.->|"replaces staged renderer; must still contain original Settings anchors"| STAGE
```

Default package composition for the renderer is `checksum-pinned-artifact-runtime` (`fidelityRuntimeComposition`). Overlay of compiled `source/` runtimes does **not** replace `dist/renderer` in that mode.

## Prerequisites

- `npm ci` and a Node `>=26.5.0 <27` toolchain.
- `npm run bootstrap` so `src/app/dist/renderer/index.html` exists. The Vite server plugin reads that file at startup. `frontend:recover` copies the same tree.
- Do not commit `.build/`, `src/app/dist/`, `recovered/`, or `dist/`.

## Commands

There is no `frontend:dev` script. Use the root scripts and Vite directly.

<CodeGroup>

```sh title="Typecheck and Vite build"
npm run typecheck
npm run frontend:build
```

```sh title="Vite dev server"
npx vite --config frontend/vite.config.ts
```

```sh title="Expand the shipped renderer for inspection"
npm run frontend:recover
```

```sh title="Package the default patched shipped UI"
npm run package
```

```sh title="Package a replacement renderer directory"
GROK_BOT_RENDERER_SOURCE=.build/frontend-shell npm run package
```

</CodeGroup>

| Command | Behavior |
| --- | --- |
| `npm run typecheck` | `tsc --project frontend/tsconfig.json` (`noEmit`, `strict`, `jsx: react-jsx`) |
| `npm run frontend:build` | `vite build --config frontend/vite.config.ts` → `.build/frontend-shell` with `base: "./"` and `sourcemap: true` |
| `npm run frontend:recover` | Unminify JS/CSS from `src/app/dist/renderer` into `recovered/frontend/app` plus reports |
| `npm run check` | Renderer typecheck + `source/` typecheck + `tests/*.test.mjs`. Does **not** run `frontend:build` |
| `npm run package` | `check` then fidelity hybrid ASAR, Router patch, reconstructed identity, ad-hoc sign, verify |

<Note>
`CONTRIBUTING.md` requires `npm run frontend:build` before sharing a change even though `check` does not run it. `vite.config.ts` comments mention `package:recovered-frontend`; that script is not in `package.json`.
</Note>

## Vite workspace layout

:::files
frontend/
  index.html                 # module entry /src/main.tsx, file-relative CSP
  vite.config.ts             # root frontend/, base ./, outDir ../.build/frontend-shell
  tsconfig.json
  manifests/                 # checked-in asset and evidence catalogs
  src/
    main.tsx                 # ProductionRenderer + health POST
    production/              # clean-source production graph
    recovered/               # evidence-backed feature reconstruction
    dev/DevShell.tsx         # inspection overlay (not the current entry)
    upstream.ts              # loads /__reconstructed_manifest + /upstream assets
:::

`frontend/index.html` uses `script-src 'self'` (no Vite HMR eval). `base: "./"` keeps every emitted script, stylesheet, font, and lazy chunk relative to `index.html` so a later copy into Electron `file://` does not need an HTTP origin.

### Dev server endpoints

The Vite plugin `serve-bootstrapped-upstream` binds `127.0.0.1:5173` (`strictPort: false`).

| Method | Path | Behavior |
| --- | --- | --- |
| `GET` | `/__reconstructed_manifest` | JSON `{ entry, styles }` rewritten to `/upstream/...` from the bootstrapped `index.html` |
| `GET` | `/__reconstructed_health` | `200` with last posted JSON, or `503` `{ ready: false }` |
| `POST` | `/__reconstructed_health` | Stores JSON body; `204` on success, `400` on invalid JSON, `405` otherwise |
| static | `/upstream/*` | `sirv` of `src/app/dist/renderer` |
| proxy | `/__sand_control/*` | `http://127.0.0.1:${SAND_DEV_CONTROL_PORT}` (default `62150`) |

<ParamField body="SAND_DEV_CONTROL_PORT" type="string" default="62150">
Loopback port for the Vite `/__sand_control` proxy and Electron `resolveDevControlPort`. Invalid or missing values fall back to `62150`.
</ParamField>

`frontend/src/main.tsx` posts health once after the first animation frame. The payload includes `cleanEntrypoint: "frontend/src/main.tsx"`, `recoveredEntrypoints: 5`, `upstreamEntry: false`, and `viteClient: import.meta.hot != null`. The POST is ignored when the reconstruction host is not running.

`DevShell` and `bootRecoveredRenderer()` exist for overlaying recovered views on the shipped bundle. They are **not** imported from `main.tsx`; the current Vite entry mounts `ProductionRenderer`.

## Recovered feature map

`frontend/src/recovered/runtime/entrypoints.ts` lists five recovered surfaces. `frontend/manifests/renderer-bootstrap.json` requires `expectedFeatureSurfaces: 5` and `expectedShippedRoutes: 11`.

| id | Name | productionChunk | recovery |
| --- | --- | --- | --- |
| `overlay:computer` | Computer | `view-ChG-6rmU.js` | `exact-placeholder` (view returns `null`) |
| `overlay:hidden-chats` | Hidden Chats | `view-Cbx1-ckK.js` | `semantic-model` |
| `view:org-chart` | Org Chart | `view-D0otXpJy.js` | `semantic-model` |
| `overlay:plugins` | Plugins | `view-B5Ug8wEm.js` | `semantic-model` |
| `overlay:settings` | Settings | `view-BRftG-LF.js` | `named-upstream-module` |

The default export of `frontend/src/recovered/features/settings/overlay/view.tsx` is `SettingsOverlaySourceBoundary`, which returns `null`. The reconstructed Settings UI lives in `SettingsModalShell` plus `desktop-surface.tsx` / `panels.tsx`. Comments starting with `@evidence` name byte or symbol anchors in the bootstrapped renderer; they are provenance notes, not imports.

### Settings reconstruction (workspace)

`SETTINGS_SECTIONS`: `general`, `router`, `usage`, `beta`. `settingsSectionsForUsage` drops `usage` when `showUsage` is false.

Router provider ids in `frontend/src/recovered/features/settings/overlay/router.ts`:

| id | label | usageSource |
| --- | --- | --- |
| `cursor` | Cursor | `cursor` |
| `claude-code` | Claude Code | `external` |
| `codex` | Codex | `external` |
| `openrouter` | OpenRouter | `external` |

<ParamField body="settings.router-provider.v1" type="string">
Workspace persistence key used by `loadRouterProvider` / `saveRouterProvider` through `bridge.agent.clientPersistence`. Payload is `{ schemaVersion: 1, provider }`. Invalid JSON or unknown ids fall back to `cursor`.
</ParamField>

Unknown ids throw `Unknown router provider.` on save. This client-persistence key is **not** what the packaged Settings Router uses.

## What `npm run package` actually injects

`scripts/lib/router-renderer-patch.mjs` edits the staged shipped chunks in place.

1. Find exactly one JS file containing the original `wDn` Settings registry (`general`, `usage`, `beta`) and insert `{id:"router",label:"Router",icon:"git-branch"}`.
2. Find exactly one JS file containing `function Sa(s){` plus the `general` and `usage` panel switches. Insert `RRouterPanel`, `RBoxRuntime`, `RRouterUsage`, and related helpers, then route `x==="router"` and replace the usage panel with `RRouterUsage`.
3. Write `dist/renderer-router-extension.json` (`schemaVersion: 1`, `mode: "original-renderer-settings-extension"`).

Injected UI talks to `window.desktop.agent.getInferenceRouter` / `setInferenceRouter`, `getBoxRuntime` / `setBoxRuntime`, and `window.desktop.secrets.list` / `upsert`. Provider values are `cursor`, `claude-code`, `codex`, `openrouter`. Features recorded in provenance: `settings-router-provider`, `settings-local-docker-vm`, `usage-current-provider`.

Missing or duplicate anchors throw `Original renderer <label> anchor is missing or ambiguous.` or `Expected one original Settings registry and panel chunk, found …`. `npm run verify` rejects packaged renderer `.map` files.

<Warning>
The workspace Settings surface writes `settings.router-provider.v1`. The packaged patch does not. Tests in `tests/publication-packaging.test.mjs` assert the patch source does **not** contain `settings.router-provider.v1`. Changing recovered `router.ts` does not change the packaged Settings Router until you change `router-renderer-patch.mjs` or ship a full renderer override that still carries the original anchors.
</Warning>

## Override the packaged renderer

<ParamField body="GROK_BOT_RENDERER_SOURCE" type="string">
Path resolved from the repository root. `scripts/lib/build-asar.mjs` requires `<path>/index.html`, deletes staged `dist/renderer`, and copies the directory. Logs `Renderer override: <abs path>`.
</ParamField>

Fidelity packaging still runs `applyOriginalRendererRouterPatch` after the copy. A Vite `frontend:build` output is therefore not a drop-in replacement unless it still contains the exact `wDn` registry and `function Sa(s){` panel anchors. The Vite build also emits source maps; `npm run verify` fails if any `dist/renderer/**/*.map` remains.

`npm run package:diagnostic` is a separate fidelity diagnostic app. It still uses the checksum-pinned renderer path, not the Vite workspace.

The unused-by-default clean-source builder (`buildProductionRenderer`) is a different Vite invocation: `configFile: false`, `sourcemap: false`, minify on, banner `"Deterministic clean-source renderer: frontend/src/main.tsx";`, and a required lazy-entry list from `renderer-bootstrap.json`. Default `npm run package` does not call it.

## Expand shipped chunks for inspection

`npm run frontend:recover` (`scripts/recover-frontend.mjs`):

1. Deletes and recreates `recovered/frontend/app` and `recovered/frontend/reports`.
2. Walks `src/app/dist/renderer`. JS/CSS go through esbuild (`minify: false`, `treeShaking: false`, external `.map`). Other files copy byte-for-byte.
3. Writes `reports/manifest.json` (per-file SHA-256 of **source** bytes), `imports.tsv`, `candidate-ui-strings.txt`, and `react-component-candidates.tsv` from `assets/index-UbX-y3il.js`.
4. Logs `Recovered <n> JavaScript/CSS assets out of <m> renderer files.`

The recovered tree is gitignored. Publication tests require `frontend/src/recovered/**` to stay addable and `/recovered/` to stay ignored.

## Evidence rule

The immutable 0.18.0 renderer is the product specification. Recovered UI may only express behavior with an inspectable artifact anchor. Do not invent screens, routes, labels, or interactions to fill gaps. Incomplete evidence stays unmapped (`return null` placeholders such as Computer overlay and `SettingsOverlaySourceBoundary`). Passing `typecheck` / `frontend:build` is not provenance.

Checked-in catalogs under `frontend/manifests/` (`renderer-bootstrap.json`, `renderer-runtime-assets.json`, `ui-evidence-anchors.json`, and related locks) are the small files the auditors and verify path consume. Do not rewrite them to match a speculative UI.

## Edit loop

<Steps>
<Step title="Hydrate the shipped renderer">
Run `npm run bootstrap` so `src/app/dist/renderer` exists. Vite and `frontend:recover` both fail without it.
</Step>
<Step title="Change recovered TypeScript, not generated payloads">
Edit `frontend/src/recovered/` or `frontend/src/production/`. Keep `@evidence` comments accurate. Do not check in `recovered/frontend/` or `src/app/dist`.
</Step>
<Step title="Typecheck and build the workspace">
Run `npm run typecheck` and `npm run frontend:build`. Confirm `.build/frontend-shell/index.html` uses relative `./assets/` URLs.
</Step>
<Step title="Decide which UI you are changing">
Workspace-only: stop here. Packaged Settings Router / local Docker toggle: edit `scripts/lib/router-renderer-patch.mjs` and keep the exact anchors. Full replacement UI: set `GROK_BOT_RENDERER_SOURCE` and accept that the Router patch and `verify` source-map rule still run.
</Step>
<Step title="Package and verify on macOS">
Run `npm run package` then `npm run verify`. Expected default renderer mode remains `checksum-pinned-artifact-runtime` with `dist/renderer-router-extension.json` present.
</Step>
</Steps>

## Failure modes

| Symptom | Cause | Fix |
| --- | --- | --- |
| Vite start throws `Bootstrapped renderer index.html has no module entry` | Missing or unexpected `src/app/dist/renderer/index.html` | `npm run bootstrap` |
| `frontend:recover` cannot read `index-UbX-y3il.js` | Hydration incomplete or renderer identity drifted | Re-bootstrap; do not invent a new main chunk name |
| Package: `Original renderer … anchor is missing or ambiguous` | Staged renderer is not the 0.18.0 Settings chunks, or `GROK_BOT_RENDERER_SOURCE` replaced them | Restore shipped renderer, or keep those exact strings in the override |
| Package: `Expected one original Settings registry and panel chunk` | Zero or multiple matching JS files | Do not split or duplicate the shipped Settings chunks |
| Verify: `Packaged renderer contains source maps` | Override or stage left `.map` files | Strip maps; default Vite `frontend:build` emits them |
| Packaged Router does not follow recovered `router.ts` | Different persistence and injection path | Patch `router-renderer-patch.mjs` or use a full override that still patches |

## Next

<CardGroup>
<Card title="Reconstruction boundary" href="/reconstruction-boundary">
Compiled `source/` runtimes, checksum-pinned renderer, reconstructed bundle id, ignored `src/app/dist`.
</Card>
<Card title="Package the macOS app" href="/package-macos-app">
`npm run package` compiles runtimes, patches Settings Router, replaces `app.asar`, signs, verifies.
</Card>
<Card title="Evidence and provenance" href="/evidence-and-provenance">
Pinned 0.18.0 identities and the evidence-only UI rule.
</Card>
<Card title="Environment variables" href="/environment-variables">
`GROK_BOT_RENDERER_SOURCE`, `SAND_DEV_CONTROL_PORT`, and related overrides.
</Card>
<Card title="Packaging failures" href="/packaging-failures">
Missing renderer patch anchors, codesign retry, verify ASAR entries.
</Card>
<Card title="npm scripts" href="/npm-scripts">
Exact `package.json` commands including `frontend:build` and `frontend:recover`.
</Card>
</CardGroup>

---

## 14. npm scripts

> Exact package.json commands: bootstrap, check, package, verify, smoke, publication:check, frontend:build, and test globs.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/14-npm-scripts.md
- Generated: 2026-08-24T05:54:46.747Z

### Source Files

- `package.json`
- `scripts/bootstrap-runtime.mjs`
- `scripts/build.mjs`
- `scripts/package-macos.mjs`
- `scripts/verify.mjs`
- `scripts/smoke.mjs`
- `scripts/verify-publication-tree.mjs`

---
title: "npm scripts"
description: "Exact package.json commands: bootstrap, check, package, verify, smoke, publication:check, frontend:build, and test globs."
---

`package.json` is the command surface for this reconstruction. The package is `grok-bot-0.18-reconstructed` at `0.18.0-reconstructed.1`, `"type": "module"`, `"private": true`, and `engines.node` `>=26.5.0 <27`. Every `npm run` entry is a Node ESM script, a `tsc` project, Vite, or a composition of those. `npm ci` also runs `postinstall`.

```json
{
  "bootstrap": "node scripts/bootstrap-runtime.mjs",
  "build": "node scripts/build.mjs",
  "check": "npm run typecheck && npm run source:typecheck && npm test",
  "frontend:build": "vite build --config frontend/vite.config.ts",
  "frontend:recover": "node scripts/recover-frontend.mjs",
  "native:patch": "node scripts/apply-third-party-patches.mjs",
  "native:build:electron": "node scripts/build-tree-sitter-electron.mjs",
  "package": "npm run check && node scripts/package-macos.mjs",
  "package:diagnostic": "npm run check && node scripts/package-fidelity-diagnostic.mjs",
  "publication:check": "node scripts/verify-publication-tree.mjs",
  "postinstall": "node scripts/apply-third-party-patches.mjs",
  "smoke": "node scripts/smoke.mjs",
  "test": "node --test tests/*.test.mjs",
  "typecheck": "tsc --project frontend/tsconfig.json",
  "source:typecheck": "tsc --project source/tsconfig.json",
  "verify": "node scripts/verify.mjs"
}
```

## Script graph

`check` is the shared gate. `package` and `package:diagnostic` always run it first. `bootstrap` is independent and must succeed before packaging can resolve the 0.18.0 Electron shell. `frontend:build` does not feed the default packaged renderer.

```mermaid
flowchart TB
  subgraph install["install"]
    PI["postinstall / native:patch"]
  end
  subgraph gate["npm run check"]
    TC["typecheck<br/>frontend/tsconfig.json"]
    STC["source:typecheck<br/>source/tsconfig.json"]
    TST["test<br/>tests/*.test.mjs"]
    TC --> STC --> TST
  end
  subgraph hydrate["bootstrap"]
    B["scripts/bootstrap-runtime.mjs"]
    SRC["ignored src/app/dist"]
    CACHE[".cache/runtime/Grok Bot.app"]
    B --> SRC
    B --> CACHE
  end
  PKG["package"] --> gate
  PKG --> PM["scripts/package-macos.mjs"]
  PM --> CACHE
  PM --> SRC
  PM --> DIST["dist/Grok Bot 0.18 Reconstructed.app"]
  BUILD["build"] --> ASAR[".build/fidelity/app.asar"]
  V["verify"] --> DIST
  SM["smoke"] --> DIST
  FB["frontend:build"] --> SHELL[".build/frontend-shell"]
  PUB["publication:check"] --> TREE["HEAD tree vs archive/init/add"]
```

## Catalog

| Script | Command | Role |
| --- | --- | --- |
| `bootstrap` | `node scripts/bootstrap-runtime.mjs` | Pin, cache, and hydrate the 0.18.0 runtime |
| `check` | `typecheck && source:typecheck && test` | Sequential typecheck + unit tests |
| `package` | `check` then `scripts/package-macos.mjs` | Darwin app bundle, ad-hoc sign, package audit |
| `verify` | `node scripts/verify.mjs` | Inspect an existing `.app` ASAR and identity |
| `smoke` | `node scripts/smoke.mjs` | Native e2e launch, 12s timeout |
| `publication:check` | `node scripts/verify-publication-tree.mjs` | Prove `git archive` export is lossless |
| `frontend:build` | `vite build --config frontend/vite.config.ts` | Design-workspace renderer to `.build/frontend-shell` |
| `test` | `node --test tests/*.test.mjs` | One-level glob under `tests/` |
| `typecheck` | `tsc --project frontend/tsconfig.json` | Renderer TS, `noEmit` |
| `source:typecheck` | `tsc --project source/tsconfig.json` | Runtime TS, `noEmit` |
| `build` | `node scripts/build.mjs` | Fidelity hybrid ASAR only (no `.app`) |
| `package:diagnostic` | `check` then `scripts/package-fidelity-diagnostic.mjs` | Isolated fidelity diagnostic bundle |
| `frontend:recover` | `node scripts/recover-frontend.mjs` | Ignored formatted copy under `recovered/frontend` |
| `native:patch` | `node scripts/apply-third-party-patches.mjs` | Same as `postinstall` |
| `native:build:electron` | `node scripts/build-tree-sitter-electron.mjs` | Rebuild tree-sitter for Electron ABI |

## `npm run bootstrap`

Resolves a Grok Bot **0.18.0** app, verifies `app.asar` SHA-256 `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58`, and extracts `dist/` into ignored `src/app/dist`.

Resolution order:

1. `GROK_BOT_018_APP` — copy that app into `.cache/runtime/Grok Bot.app` after `CFBundleShortVersionString` is `0.18.0`.
2. Cached app at `.cache/runtime/Grok Bot.app`.
3. Git LFS DMG `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` (SHA-256 `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb`), copied to `.cache/downloads/Grok_Bot_0.18.0.dmg`.
4. Public URL `https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg`.

DMG extract uses `hdiutil attach` on `Grok Bot.app`. Hydration requires `dist/electron-main/main.cjs`, `dist/host/host-main.cjs`, and `dist/renderer/index.html`.

<RequestExample>
```bash
npm run bootstrap
```
</RequestExample>

<ResponseExample>
```text
Runtime ready: <repo>/.cache/runtime/Grok Bot.app
Checksum-pinned source payload ready: <repo>/src/app/dist (<sha256>)
The checksum-pinned app supplies only the Electron shell, ABI-matched native dependencies, and explicitly documented build fallbacks.
```
</ResponseExample>

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute path to an existing 0.18.0 `Grok Bot.app`. Version mismatch fails before hydration.
</ParamField>

Failure cases: LFS pointer-sized archive, DMG checksum mismatch (`Run git lfs pull before bootstrapping.`), HTTP download failure, missing required `app.asar` files, `Expected Grok Bot 0.18.0`. Later packaging without a cache throws `Missing 0.18.0 runtime. Run npm run bootstrap first.`

## `npm run check`

Runs **in order** and stops on the first non-zero exit:

1. `typecheck` — `tsc --project frontend/tsconfig.json` (`include`: `src`, `vite.config.ts`; `noEmit: true`).
2. `source:typecheck` — `tsc --project source/tsconfig.json` (`include`: `**/*.ts`; `noEmit: true`).
3. `test` — `node --test tests/*.test.mjs`.

This is the required local gate in `CONTRIBUTING.md` together with `frontend:build`. `package` embeds `check`; running `check` alone does not compile runtimes or write `dist/`.

## `npm test` glob

Exact script: `node --test tests/*.test.mjs`. The glob is one directory deep. Nested files under `tests/` are not collected.

| File | What it pins |
| --- | --- |
| `tests/backend-mcp-exec-json.test.mjs` | MCP exec JSON |
| `tests/codex-direct-responses.test.mjs` | Codex Responses client |
| `tests/inference-router-transcript.test.mjs` | Router transcript store |
| `tests/publication-bootstrap.test.mjs` | Binding paths into `source/`; hydration checksum |
| `tests/publication-packaging.test.mjs` | Verify authority is the `.app`; default fidelity renderer; ignore rules |
| `tests/reconstructed-updater-guard.test.mjs` | `SAND_DISABLE_*` packaging guard |
| `tests/research-archives.test.mjs` | LFS installer inventory `artifacts.json` schemaVersion 1 |
| `tests/router-settings.test.mjs` | Settings Router wiring |

## `npm run package`

Darwin only. Non-macOS throws `The reconstructed macOS application can only be packaged on macOS.`

After `check`, `scripts/package-macos.mjs`:

1. Compiles the fidelity hybrid ASAR (`buildFidelityReconstructedAsar`): clean `source/` runtimes where activated, checksum-pinned shipped renderer, then `applyOriginalRendererRouterPatch`.
2. Audits the official cached app as **reference only**.
3. `ditto` of the runtime into `dist/`, then `xattr -cr` to drop quarantine.
4. Replaces `Contents/Resources/app.asar` and `.unpacked`.
5. Sets `CFBundleIdentifier` `com.anysphere.sand.reconstructed`, `CFBundleDisplayName` `Grok Bot 0.18 Reconstructed`, URL scheme `sand` only; removes `ElectronAsarIntegrity`.
6. Ad-hoc `codesign` (one retry on nested-framework failure), then `codesign --verify --deep --strict`.
7. `verifyReconstructedMacPackage`.

Default output: `dist/Grok Bot 0.18 Reconstructed.app`. Default packaging keeps the checksum-pinned renderer (publication test: `default packaging keeps the polished checksum-pinned renderer`).

<ParamField body="GROK_BOT_OUTPUT_APP_NAME" type="string">
Basename of the output `.app` under `dist/`. Default `Grok Bot 0.18 Reconstructed.app`.
</ParamField>

<ParamField body="GROK_BOT_RENDERER_SOURCE" type="string">
Directory containing `index.html` copied into staged `dist/renderer` during ASAR staging. Default `npm run package` still uses the fidelity (checksum-pinned) renderer composition unless this override is set. `frontend:build` output is not selected automatically.
</ParamField>

<ParamField body="GROK_BOT_HOST_BINDINGS_MANIFEST" type="string">
Optional host production-binding manifest path.
</ParamField>

<ParamField body="GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST" type="string">
Optional Electron-main binding manifest. Default `manifests/reconstruction/electron-main-production-bindings-manifest.json` when that file exists.
</ParamField>

## `npm run verify`

Validates a packaged `.app` (default `dist/Grok Bot 0.18 Reconstructed.app`), not `.build/app.asar`.

Usage: `node scripts/verify.mjs [--app /absolute/path/to/App.app]`. Invalid argv throws `Usage: node scripts/verify.mjs [--app /absolute/path/to/App.app]`.

Hard checks include:

- At least **1,000** surviving `// src/` evidence markers in hydrated `electron-main` + `host` mains.
- Required ASAR entries: `dist/electron-main/main.cjs`, preloads, `node-agent-coordinator`, `host-main.cjs`, workers, `local-exec-daemon`, `dist/renderer/index.html`, `dist/reconstruction-build.json`, `dist/runtime-composition-audit.json`, `package.json`.
- Unpacked natives: `better_sqlite3.node`, `sand-webauthn-signer`, tree-sitter Node bindings.
- No renderer `.map` files in the ASAR.
- Packaged app icon matches `frontend/manifests/renderer-runtime-assets.json`.
- Bundle id `com.anysphere.sand.reconstructed`, display name `Grok Bot 0.18 Reconstructed`, `sand` URL scheme, no leftover `ElectronAsarIntegrity`.
- `codesign --verify --deep --strict`.

Renderer mode must be `checksum-pinned-artifact-runtime` or `clean-source`; anything else throws `Unsupported packaged renderer mode`.

## `npm run smoke`

`scripts/smoke.mjs` calls `runNativeE2E` on the default output app with `timeoutMs: 12_000` and `structuralOnly: false`. Launch env forces `SAND_DISABLE_UPDATES`, `SAND_DISABLE_TELEMETRY`, and `SAND_DISABLE_ANALYTICS` to `"1"`.

<ResponseField name="process.exitCode" type="number">
`0` when `report.status === "pass"`, `2` when `"prerequisite"`, `1` otherwise. Lines look like `PASS check: detail` then `Smoke verification: PASS`.
</ResponseField>

## `npm run publication:check`

On the committed tree: `git archive HEAD` → extract → `git init` → `git add --all` → compare `rev-parse HEAD^{tree}` with `write-tree`. Mismatch reports omitted and unexpected paths (first 20 each). Also requires exported `frontend/src/recovered/ui/sand-form-primitives.css` to be non-empty so ignore rules did not drop tracked frontend source.

Success: `Publication export preserves <N> files and tree <oid>.`

## `npm run frontend:build`

`vite build --config frontend/vite.config.ts`: `base: "./"`, `outDir` `.build/frontend-shell`, `emptyOutDir: true`, `sourcemap: true`. Required by contributing, independent of `package`. The packaged UI stays the patched shipped renderer unless `GROK_BOT_RENDERER_SOURCE` points at a built tree.

`frontend:recover` writes an ignored formatted inspection copy under `recovered/frontend` from bootstrapped `src/app/dist/renderer`.

## Other scripts

### `build`

Same fidelity ASAR as packaging (`buildFidelityReconstructedAsar`) without copying into a `.app`. Logs `Reconstructed ASAR: …` and `Renderer mode: checksum-pinned upstream 0.18.0 payload`.

### `package:diagnostic`

Darwin only. Refuses if `.build/fidelity/e2e-candidate.json` or `release-candidate.json` exists. Stages under `.build/diagnostic-fidelity`.

### `postinstall` / `native:patch`

SHA-gated edits to `@connectrpc/connect` transports and `node_modules/tree-sitter/binding.gyp`. Unexpected input hashes throw `Refusing to patch unexpected …`. Idempotent when already at the patched digest.

### `native:build:electron`

Requires `ELECTRON_HEADERS_DIR` matching Electron `42.1.0` (`NODE_MODULE_VERSION` 146, Node 24.15.0 headers). Rebuilds `tree-sitter` and `tree-sitter-bash` for the packaged Electron ABI.

## Typical sequence

<Steps>
<Step title="Install and patch">
`npm ci` (Node 26.5.x). `postinstall` applies third-party patches.
</Step>
<Step title="Hydrate the pinned runtime">
`git lfs pull` then `npm run bootstrap`. Confirm `src/app/dist` and `.cache/runtime/Grok Bot.app`.
</Step>
<Step title="Gate">
`npm run check` and `npm run frontend:build`.
</Step>
<Step title="Package and prove">
On macOS: `npm run package`, then `npm run verify` and optionally `npm run smoke`. Before a public remote: `npm run publication:check`.
</Step>
</Steps>

<Warning>
Do not weaken checksum, bundle identity, codesign, or `publication:check` gates to force a green run. Generated trees (`.cache`, `.build`, `dist`, `src/app/dist`, `recovered`) stay untracked.
</Warning>

## Related pages

<CardGroup>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
`GROK_BOT_018_APP`, cache, LFS DMG, public URL, and `src/app/dist` hydration.
</Card>
<Card title="Package the macOS app" href="/package-macos-app">
ASAR replace, reconstructed identity, ad-hoc sign, and package audit.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
Required ASAR paths, evidence markers, 12s native e2e, updater guards.
</Card>
<Card title="Publication export" href="/publication-export">
`publication:check` tree equality, LFS push, NOTICE review.
</Card>
<Card title="Work on the frontend reconstruction" href="/edit-frontend-workspace">
Vite workspace vs checksum-pinned packaged renderer.
</Card>
<Card title="Environment variables" href="/environment-variables">
`GROK_BOT_018_APP`, `GROK_BOT_OUTPUT_APP_NAME`, `GROK_BOT_RENDERER_SOURCE`, `SAND_DISABLE_*`.
</Card>
<Card title="Contributing" href="/contributing">
Required `check` / `package` commands and what not to commit.
</Card>
</CardGroup>

---

## 15. Settings schema

> settings.json version 1 fields: inferenceProvider, inferenceRouterUsage, boxRuntime, MCP instruction maps, and atomic persist path.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/15-settings-schema.md
- Generated: 2026-08-24T05:55:43.455Z

### Source Files

- `source/shared/node/settings/sand-settings-store.ts`
- `source/shared/inference-router.ts`
- `source/shared/box-runtime.ts`
- `source/shared/host-settings.ts`
- `source/host/extensions/settings/settings-service.ts`
- `source/shared/local-tool-permission.ts`

---
title: "Settings schema"
description: "settings.json version 1 fields: inferenceProvider, inferenceRouterUsage, boxRuntime, MCP instruction maps, and atomic persist path."
---

`SandSettingsStore` owns `settings.json` as a version-`1` `SandStoredSettings` document. `SettingsService` constructs the store at `join(getSandRootDir(), "settings.json")`. Settings → Router writes `inferenceProvider` and `boxRuntime` through `window.desktop.agent.setInferenceRouter` / `setBoxRuntime`; routed turns append `inferenceRouterUsage` via `recordInferenceUsage`. MCP instruction maps live in the same file and are mirrored to the box through `setHostSettings`. `boxRuntime` is store-only: it is not part of `HostSettingsUpdate`.

## Persist path

The file name is always `settings.json` under the resolved Sand data root.

| Resolution order | Condition | Root |
| --- | --- | --- |
| 1 | `SAND_DATA_ROOT` is a non-empty absolute path | that directory |
| 2 | `--user-data-dir` or `SAND_USER_DATA_DIR` is set | `<user-data-dir>/sand-data` |
| 3 | Packaged production (`SAND_PACKAGED=1`, not lab) | `~/.grokbot` |
| 4 | Unpackaged or lab | `~/.cursor/<variant>` (`sand-dev` or `sand-lab`) |

Packaged default file: `~/.grokbot/settings.json`. `settings.json` is also a signature entry for data-root settlement (alongside `gateway.json`, `host-secrets.json`, `host.lock`, and `.grokbot-data-root-v1`).

## Atomic write

Every persist pretty-prints JSON (`JSON.stringify(..., null, 2)`), writes a sibling temp file, then rename-replaces the live document:

```text
<settings.json>.<pid>.tmp  --write-->  renameSync -->  settings.json
```

`mkdirSync` creates the parent directory recursively. `load()` returns `emptySettings()` when the file is missing, JSON is invalid, or `version` is not `1`. Pending migration `downgrade-persisted-max-fast` runs after a successful parse and re-persists.

```mermaid
flowchart TB
  subgraph ui [Packaged Settings UI]
    Router["Settings → Router"]
  end
  subgraph rpc [Desktop RPC]
    IR["getInferenceRouter / setInferenceRouter"]
    BR["getBoxRuntime / setBoxRuntime"]
    Sync["syncHostSettingsToBox"]
  end
  subgraph host [Host]
    SS["SettingsService.setHostSettings"]
    Rec["recordInferenceUsage"]
  end
  subgraph store [SandSettingsStore]
    Persist["write pid tmp then rename"]
  end
  subgraph disk [Data root]
    File["settings.json"]
  end
  Router --> IR
  Router --> BR
  IR --> Persist
  BR --> Persist
  IR -->|"inferenceProvider"| Sync
  Sync --> SS
  SS --> Persist
  Rec --> Persist
  Persist --> File
```

## Document version and empty document

<ParamField body="version" type="1" required>
Must be exactly `1`. Any other value discards the file contents and loads `emptySettings()`.
</ParamField>

`emptySettings()` seeds:

| Field | Default |
| --- | --- |
| `version` | `1` |
| `mcpBoxServers` | `[]` |
| `autoUpdateWhenIdleOptIn` | `false` |
| `egressTunnelEnabled` | `false` |
| `webauthnProxyEnabled` | `true` |
| `mcpCustomInstructions` | `{}` |
| `mcpCustomInstructionsByServerId` | `{}` |
| `mcpDisabledToolsByServerId` | `{}` |
| `conciergeConsent` | `"unset"` |
| `settingsMigrations` | `["downgrade-persisted-max-fast"]` |

Optional fields (`inferenceProvider`, `inferenceRouterUsage`, `boxRuntime`, models, timezone, onboarding, sidebar) are omitted until first write. Getters apply in-memory defaults when the keys are absent.

## Featured persisted fields

### `inferenceProvider`

<ParamField body="inferenceProvider" type='"cursor" \| "claude-code" \| "codex" \| "openrouter"'>
Stored only when `isSandInferenceProvider` accepts the value. Getter default is `"cursor"`. Unknown strings are dropped on parse.
</ParamField>

Writes:

| Surface | Behavior |
| --- | --- |
| `window.desktop.agent.setInferenceRouter({ provider })` | Rejects unknown ids with `Unknown inference provider.`, then `setInferenceProvider` and `syncHostSettingsToBox({ inferenceProvider })`. Sync failure is swallowed; the local file still holds the provider. |
| `SettingsService.setHostSettings({ inferenceProvider })` | Same store write when the value is a known provider. |

Packaged Settings → Router reads `getInferenceRouter`, which prefers the desktop store provider and returns box usage when available. The `frontend/` workspace also has a client-persistence helper at key `settings.router-provider.v1`; packaged UI does not use that key for `settings.json`.

### `inferenceRouterUsage`

Local meter, `schemaVersion: 1`. Not a `HostSettingsUpdate` field. Host `getHostSettings()` returns it; turns append it through `recordInferenceUsage`.

<ResponseField name="schemaVersion" type="1">
Always reconstructed as `1` on parse. Incoming usage objects do not have to carry a version; counters are copied onto `emptySandInferenceRouterUsage()`.
</ResponseField>

<ResponseField name="providers" type="Record<SandInferenceProvider, usage row>">
One row per `cursor`, `claude-code`, `codex`, `openrouter`.
</ResponseField>

Each provider row:

| Field | Type | Parse / write rules |
| --- | --- | --- |
| `requests` | number | Safe integer ≥ 0, else `0`. Incremented by `1` per `recordInferenceUsage`. |
| `inputTokens` | number | Same. Added from finite ≥ 0 values, `Math.round`. |
| `outputTokens` | number | Same. |
| `cacheReadTokens` | number | Same. |
| `cacheWriteTokens` | number | Same. |
| `lastUsedAt` | string \| null | ISO timestamp on record; non-strings become `null`. |

Unknown provider keys in the JSON object are ignored. Missing usage loads `emptySandInferenceRouterUsage()` (all zeros, `lastUsedAt: null`).

### `boxRuntime`

<ParamField body="boxRuntime" type='"remote" \| "local-docker"'>
Getter default is `"remote"`. Unknown values are dropped. Not included in `HostSettingsUpdate` or `getHostSettings()`.
</ParamField>

`setBoxRuntime` persists first, then starts `grok-bot-local-vm` or stops it. On start/stop failure the store reverts (`local-docker` → `remote`, or the inverse) and the RPC throws. A successful switch then restarts the coordinator.

### MCP instruction maps

| Field | Shape | Constraints |
| --- | --- | --- |
| `mcpCustomInstructions` | `Record<string, string>` | Keys are connector display names. Values clamped to `MCP_CUSTOM_INSTRUCTIONS_MAX_LENGTH` (`500`). Empty strings are kept only when a default instruction exists for that name (today: `hex`); otherwise the key is deleted. |
| `mcpCustomInstructionsByServerId` | `Record<string, string>` | Keys must match `/^[1-9]\d*$/` (positive integer server ids, no leading zeros). Values clamped to 500 characters. |
| `mcpDisabledToolsByServerId` | `Record<string, string[]>` | Same numeric-id keys. Tool names are unique non-empty strings; empty lists are omitted. |
| `mcpCustomInstructionsAccountScope` | string | Non-empty string when present. Scopes MCP maps to an account. |
| `mcpBoxServers` | `string[]` | Unique non-empty names. |

`setMcpCustomInstructionByServerId` can mirror the clamped value into the legacy name map. `migrateMcpCustomInstructionToServerId` copies a legacy name into the id map only when the id is not already set.

Account switch (`scopeToAccount` with a different scope, or `clearAccountScope`) clears `mcpCustomInstructions`, `mcpCustomInstructionsByServerId`, and `mcpDisabledToolsByServerId`. A scope change also drops `autoReviewInstructions`, `agentDefaultModel`, `computerUseModel`, `localToolPermission`, and `localToolPermissionCeiling`.

## Other version-1 fields

| Field | Type | Notes |
| --- | --- | --- |
| `autoUpdateWhenIdleOptIn` | boolean | Default `false`. Stored only as exact `true`. |
| `egressTunnelEnabled` | boolean | Default `false`. |
| `webauthnProxyEnabled` | boolean | Default `true` unless JSON is exactly `false`. |
| `conciergeConsent` | `"unset"` \| `"allowed"` \| `"denied"` | Other values become `"unset"`. |
| `settingsMigrations` | string[] | Includes `downgrade-persisted-max-fast` after first load. |
| `hasSeenOnboarding` | boolean | Optional. |
| `hasSeenOnboardingAccountScope` | string | Optional; rewritten when onboarding is set. |
| `updateTrackOverride` | `"stable"` \| `"nightly"` \| `"dogfood"` | `nightly` is coerced to `stable` on read. |
| `themePreference` | `"system"` \| `"light"` \| `"dark"` | Getter default `"system"`. |
| `agentDefaultModel` | `{ modelId, maxMode, parameters[] }` | Reads force `maxMode: true`. Migration sets parameter `fast` to `"false"`. |
| `computerUseModel` | same selection shape | Stored as given. |
| `notifications` | object | Reads/writes collapse to `{ isEnabled: false }`. Host always returns `SAND_DISABLED_NOTIFICATION_CONFIG`. |
| `userTimeZone` | string | Detected IANA zone. Host accepts `""` to clear or a valid IANA id. |
| `userTimeZoneOverride` | string | Overrides detection. Effective zone is override ?? detected. |
| `autoReviewInstructions` | `{ isEnabled, allowInstructions, blockInstructions }` | Default enabled with empty lists. Lists capped at 20 unique entries, 1000 chars each. |
| `localToolPermission` | `"always"` \| `"ask"` \| `"never"` | Default `"ask"`. Effective value is `min(choice, ceiling)` by rank `never < ask < always`. |
| `localToolPermissionCeiling` | same enum | Optional admin cap. |
| `pinnedAgentIds` | string[] | Unique non-empty ids. |
| `sidebarSections` | `{ id, name, agentIds, isCollapsed? }[]` | Fold state is carried across updates. |

## Host update surface vs store

`HostSettingsUpdate` can set MCP maps, timezone, models, auto-review, local-tool permission, webauthn, pins, sidebar, onboarding, and `inferenceProvider`. It does **not** set `boxRuntime` or `inferenceRouterUsage`. `featureFlagOverrides` on the update object notify listeners only; they are not written to `settings.json`.

## Example document

<ResponseExample>

```json
{
  "version": 1,
  "mcpBoxServers": [],
  "autoUpdateWhenIdleOptIn": false,
  "egressTunnelEnabled": false,
  "webauthnProxyEnabled": true,
  "mcpCustomInstructions": {},
  "mcpCustomInstructionsByServerId": {
    "12": "Prefer CSV exports over screenshot charts."
  },
  "mcpDisabledToolsByServerId": {
    "12": ["browser_navigate"]
  },
  "conciergeConsent": "unset",
  "settingsMigrations": ["downgrade-persisted-max-fast"],
  "inferenceProvider": "codex",
  "inferenceRouterUsage": {
    "schemaVersion": 1,
    "providers": {
      "cursor": {
        "requests": 0,
        "inputTokens": 0,
        "outputTokens": 0,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": null
      },
      "claude-code": {
        "requests": 0,
        "inputTokens": 0,
        "outputTokens": 0,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": null
      },
      "codex": {
        "requests": 4,
        "inputTokens": 12000,
        "outputTokens": 3100,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": "2026-08-24T18:04:11.000Z"
      },
      "openrouter": {
        "requests": 0,
        "inputTokens": 0,
        "outputTokens": 0,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": null
      }
    }
  },
  "boxRuntime": "remote",
  "localToolPermission": "ask"
}
```

</ResponseExample>

## Load and write failures

<AccordionGroup>
<Accordion title="Corrupt or wrong-version file">
`load()` returns `emptySettings()` and does not throw. A later persist overwrites the file with a valid version-`1` document.
</Accordion>
<Accordion title="Unknown inference provider">
Desktop RPC throws `Unknown inference provider.` Store parse drops the key; getter returns `"cursor"`.
</Accordion>
<Accordion title="Unknown box runtime">
Desktop RPC throws `Unknown box runtime.` Store parse drops the key; getter returns `"remote"`. A failed Docker start/stop reverts the persisted mode before rethrowing.
</Accordion>
<Accordion title="MCP instruction overflow">
Values longer than 500 characters are sliced. Disabled-tool lists drop empty names and duplicates.
</Accordion>
<Accordion title="Notifications">
Any stored notification object is rewritten to `{ "isEnabled": false }` on read. Host never re-enables notifications from this file.
</Accordion>
</AccordionGroup>

<Warning>
Do not treat `settings.json` as a secrets store. OpenRouter keys go through `window.desktop.secrets` / `host-secrets.json`. Codex and Claude Code auth stay in those CLIs' own files.
</Warning>

## Related pages

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, transcript store, and usage schemaVersion 1.
</Card>
<Card title="Box runtime" href="/box-runtime">
`remote` vs `local-docker`, coordinator restart, loopback gateway.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
`getInferenceRouter`, `setInferenceRouter`, `getBoxRuntime`, `setBoxRuntime`.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Settings → Router persist path and provider credentials.
</Card>
<Card title="Enable the local Docker sandbox" href="/enable-local-docker">
Toggle that writes `boxRuntime` and starts `grok-bot-local-vm`.
</Card>
<Card title="Route Grok Bot plugin tools" href="/route-mcp-tools">
How MCP maps and routed tools are applied at turn time.
</Card>
<Card title="Environment variables" href="/environment-variables">
`SAND_DATA_ROOT`, `SAND_USER_DATA_DIR`, and related overrides.
</Card>
</CardGroup>

---

## 16. Environment variables

> GROK_BOT_018_APP, GROK_BOT_OUTPUT_APP_NAME, SAND_DISABLE_*, OPENROUTER_API_KEY, SAND_OPENROUTER_MODEL, CODEX_HOME, CLAUDE_CODE_PATH, and binding-manifest overrides.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/16-environment-variables.md
- Generated: 2026-08-24T05:59:56.650Z

### Source Files

- `scripts/lib/config.mjs`
- `scripts/bootstrap-runtime.mjs`
- `scripts/lib/build-asar.mjs`
- `source/host/extensions/inference/provider-session.ts`
- `source/shared/node/inference-router-local.ts`
- `scripts/clean-build.mjs`

---
title: "Environment variables"
description: "GROK_BOT_018_APP, GROK_BOT_OUTPUT_APP_NAME, SAND_DISABLE_*, OPENROUTER_API_KEY, SAND_OPENROUTER_MODEL, CODEX_HOME, CLAUDE_CODE_PATH, and binding-manifest overrides."
---

`process.env` is read in three layers: Node packaging scripts (`scripts/bootstrap-runtime.mjs`, `scripts/lib/config.mjs`, `scripts/lib/build-asar.mjs`, `scripts/clean-build.mjs`), the reconstructed Electron-main guard prepended into `dist/electron-main/main.cjs`, and the host inference clients in `source/host/extensions/inference/provider-session.ts` plus `source/shared/node/inference-router-local.ts`. Unset packaging keys fall back to checksum-pinned cache paths; unset inference keys fall back to Settings secrets or local CLI homes; reconstructed `SAND_DISABLE_*` defaults use `??=` so an explicit launch value still wins.

## Bootstrap and package identity

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute or resolvable path to an existing `Grok Bot.app`. `npm run bootstrap` validates `CFBundleShortVersionString` against `0.18.0`, copies the tree with `ditto` into `.cache/runtime/Grok Bot.app`, then hydrates `src/app/dist` from that app’s `app.asar`. `resolveRuntimeApp()` uses the same variable during package/ASAR builds without downloading a DMG.
</ParamField>

<ParamField body="GROK_BOT_OUTPUT_APP_NAME" type="string">
Optional output bundle file name. `scripts/lib/config.mjs` takes `path.basename()` of the trimmed value and writes `dist/<name>`. Default is `Grok Bot 0.18 Reconstructed.app`.
</ParamField>

```mermaid
flowchart TD
  start["npm run bootstrap"] --> env{"GROK_BOT_018_APP trimmed?"}
  env -->|yes| cacheApp["validate 0.18.0 then ditto into .cache/runtime/Grok Bot.app"]
  env -->|no| cached{"cachedRuntimeApp exists?"}
  cached -->|yes| validateCached["validate .cache/runtime/Grok Bot.app"]
  cached -->|no| dmg["LFS archived DMG if checksum matches, else public 0.18.0 URL"]
  dmg --> extract["hdiutil attach then cache Grok Bot.app"]
  cacheApp --> hydrate["hydrate src/app/dist from app.asar"]
  validateCached --> hydrate
  extract --> hydrate
```

| Variable | Consumer | Default when unset | Constraint |
|---|---|---|---|
| `GROK_BOT_018_APP` | `scripts/bootstrap-runtime.mjs`, `scripts/lib/runtime.mjs` | `.cache/runtime/Grok Bot.app`, else DMG extract | Must be 0.18.0; must contain `Contents/MacOS/Grok Bot` and `app.asar.unpacked` |
| `GROK_BOT_OUTPUT_APP_NAME` | `scripts/lib/config.mjs` | `Grok Bot 0.18 Reconstructed.app` | Only the basename is used |

<RequestExample>
```bash
GROK_BOT_018_APP="/Applications/Grok Bot.app" npm run bootstrap
GROK_BOT_OUTPUT_APP_NAME="Grok Bot 0.18 Reconstructed.app" npm run package
```
</RequestExample>

<Warning>
`GROK_BOT_018_APP` that is not `0.18.0` fails with `Expected Grok Bot 0.18.0, got <version> at <path>`. Packaging without a cached runtime and without this variable fails with `Missing 0.18.0 runtime. Run npm run bootstrap first.`
</Warning>

## Reconstructed `SAND_DISABLE_*` guard

`scripts/lib/build-asar.mjs` exports `reconstructedUpdaterGuard` and prepends it onto reconstructed Electron-main source (artifact fallback and clean production bundle when `reconstructedPackage: true`). The injected prefix is:

```js
process.env.SAND_DISABLE_UPDATES ??= "1";
process.env.SAND_DISABLE_SENTRY ??= "1";
process.env.SAND_DISABLE_TELEMETRY ??= "1";
```

`??=` assigns only when the key is `null` or `undefined`. A launch-time value is kept. Runtime checks treat disable as exact `"1"`:

| Variable | Effect when `"1"` | Effect when not `"1"` |
|---|---|---|
| `SAND_DISABLE_UPDATES` | `computeUpdateDisabledReason` sets `envDisabled: true` | Updater disable reason does not come from this env key |
| `SAND_DISABLE_SENTRY` | Sentry adapters skip init (`SAND_DISABLE_SENTRY !== "1"` is false) | Sentry can initialize |
| `SAND_DISABLE_TELEMETRY` | Desktop/host structured-log telemetry is disabled | Telemetry adapters run |

`npm run smoke` also forces `SAND_DISABLE_ANALYTICS=1` on the isolated native process. That key is **not** part of `reconstructedUpdaterGuard`.

## Inference credentials and models

Host routing reads credentials from the process environment first, then from `box-secrets.json` under `getSandRootDir()` for OpenRouter only. Settings → Router stores the OpenRouter secret as `OPENROUTER_API_KEY` via `window.desktop.secrets.upsert`.

<ParamField body="OPENROUTER_API_KEY" type="string" required>
Required when `inferenceProvider` is `openrouter`. Resolution: `process.env.OPENROUTER_API_KEY`, else `persistedSecrets().OPENROUTER_API_KEY`. Empty after both lookups throws `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.`
</ParamField>

<ParamField body="SAND_OPENROUTER_MODEL" type="string">
OpenRouter model id passed to `createOpenAI({ baseURL: "https://openrouter.ai/api/v1" }).chat(...)`. Default `openai/gpt-5.2`.
</ParamField>

<ParamField body="CODEX_HOME" type="string">
Directory for Codex `auth.json` and `config.toml`. Default `~/.codex`. `auth.json` must be a non-symlink regular file with no group/other bits (`mode & 0o077 === 0`). Tokens must have `auth_mode === "chatgpt"` plus non-empty `access_token`, `refresh_token`, `id_token`, and `account_id`.
</ParamField>

<ParamField body="CLAUDE_CODE_PATH" type="string">
First candidate for the Claude Code executable. If missing, search continues at `~/.local/bin/claude`, `~/.claude/local/claude`, `PATH`, `/opt/homebrew/bin/claude`, `/usr/local/bin/claude`. Missing executable throws `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.`
</ParamField>

| Variable | Provider | Default | Notes |
|---|---|---|---|
| `OPENROUTER_API_KEY` | `openrouter` | none | Env beats Settings secret |
| `SAND_OPENROUTER_MODEL` | `openrouter` | `openai/gpt-5.2` | Used for both `getModelId()` and the chat client |
| `CODEX_HOME` | `codex` | `~/.codex` | Auth file `$CODEX_HOME/auth.json`; model/effort may come from `$CODEX_HOME/config.toml` |
| `SAND_CODEX_MODEL` | `codex` | `config.toml` `model`, else `gpt-5.4` | Env wins over TOML |
| `SAND_CODEX_REASONING_EFFORT` | `codex` | `config.toml` `model_reasoning_effort` | Allowed: `minimal`, `low`, `medium`, `high`, `xhigh` |
| `CLAUDE_CODE_PATH` | `claude-code` | discovered CLI | Passed as `pathToClaudeCodeExecutable` to `@anthropic-ai/claude-agent-sdk` |
| `SAND_CLAUDE_MODEL` | `claude-code` | SDK default | Optional `query()` `model` option |
| `CODEX_PATH` | status only | discovered `codex` binary | Codex HTTP calls do not spawn this CLI |
| `ANTHROPIC_API_KEY` | `claude-code` status | none | Marks `authenticated` if `.claude/.credentials.json` is absent |

<Note>
The renderer Router panel does not collect `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. Codex uses the private ChatGPT login already stored by Codex; Claude Code uses the local CLI login (or `ANTHROPIC_API_KEY` for status only).
</Note>

## Binding-manifest overrides

Clean production activation is fail-closed. Env (or a CLI path on the activation scripts) supplies extra `schemaVersion` 1 JSON; unbound required slots keep the artifact fallback.

<ParamField body="GROK_BOT_HOST_BINDINGS_MANIFEST" type="string">
Path to a host production binding manifest. `buildProductionHostIfSupplied` and `buildCleanDistribution` read `process.env.GROK_BOT_HOST_BINDINGS_MANIFEST`. There is no checked-in default file. Manifest must be `{ schemaVersion: 1, bindings: [...] }`. Direct CLI: `node scripts/host-production-activation.mjs [manifest]`.
</ParamField>

<ParamField body="GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST" type="string">
Path to an Electron-main production binding manifest. Resolution order for packaging: this env var, else `manifests/reconstruction/electron-main-production-bindings-manifest.json` if that file exists. Direct activation CLI prefers `argv[2]` over the env var. Residual unbound slots stay fail-closed; no native default is inferred for missing slots.
</ParamField>

| Surface | Env | Default path | Failure |
|---|---|---|---|
| Host `dist/host/host-main.cjs` | `GROK_BOT_HOST_BINDINGS_MANIFEST` | none | `incomplete-evidence-derived-manifest` listing unbound mandatory bindings |
| Electron-main `dist/electron-main/main.cjs` | `GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST` | `manifests/reconstruction/electron-main-production-bindings-manifest.json` | `Supply a schemaVersion 1 manifest for those slots through GROK_BOT_ELECTRON_MAIN_BINDINGS_MANIFEST` |

Clean Electron-main packaging still prepends `reconstructedUpdaterGuard` after esbuild when `reconstructedPackage` is true.

## Renderer and lab-build packaging

These keys change what `buildAsar()` stages. They are packaging-time, not Settings.

| Variable | Value | Behavior |
|---|---|---|
| `GROK_BOT_RENDERER_SOURCE` | path relative to repo root | Replaces staged `dist/renderer` after requiring `index.html` at that path |
| `GROK_BOT_BUILD_DEV_APP` | `"1"` | Sets staged `package.json` `sandLab: true` and `productName` `Grok Bot 0.18 Dev`; enables DevTools / control-server seams |
| `GROK_BOT_RECONSTRUCTED_DEV` | `"1"` | Runtime seam: treat as sand-lab, skip single-instance lock, open DevTools gate. Stripped from `npm run smoke` |

Without `GROK_BOT_RENDERER_SOURCE`, the packaged UI remains the patched shipped renderer.

## Isolated native launch env

`createNativeTestEnvironment()` copies the parent env, deletes the production denylist, then sets:

```text
SAND_USER_DATA_DIR=<absolute isolated root>
SAND_DATA_ROOT=<isolated root>/sand-data
SAND_DISABLE_UPDATES=1
SAND_DISABLE_TELEMETRY=1
SAND_DISABLE_ANALYTICS=1
```

Denylist keys include `GROK_BOT_RECONSTRUCTED_DEV`, `SAND_DEV_LOGIN`, `SAND_BACKEND_URL`, `CURSOR_API_BASE_URL`, `SAND_HOST_GATEWAY_URL`, `SAND_HOST_GATEWAY_TOKEN`, `SAND_FEATURE_GATE_OVERRIDES`, and `SAND_MODEL_EXPERIMENT_OVERRIDE`. `SAND_USER_DATA_DIR` is also honored by desktop bootstrap (`--user-data-dir` wins over the env key). `SAND_DATA_ROOT` must be an absolute path to override `getSandRootDir()`.

Local Docker injects container env (`SAND_BOX_AUTO_UPDATE=0`, `SAND_GATEWAY_BIND_HOST=0.0.0.0`, `SAND_HOST_PORT=1340`, …) when creating `grok-bot-local-vm`. Those are not packaging-script variables.

## Errors

| Condition | Message / status |
|---|---|
| `GROK_BOT_018_APP` wrong version | `Expected Grok Bot 0.18.0, got …` |
| Incomplete `.app` | `Incomplete Grok Bot runtime at …` |
| No runtime for package | `Missing 0.18.0 runtime. Run npm run bootstrap first.` |
| Missing OpenRouter key | `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.` |
| Codex `auth.json` world-readable, symlink, or non-file | `Codex login credentials must be a private direct regular file.` |
| Codex not ChatGPT-signed-in | `Codex is not signed in with ChatGPT. Run codex login, then reopen Grok Bot.` |
| Claude CLI missing | `Claude Code is not installed. …` |
| Host/Electron-main manifest not `schemaVersion` 1 | `… binding manifest must use schemaVersion 1 and a bindings array` |
| Renderer override missing `index.html` | `readFile` fails on the override path |
| Dev/runtime seam anchors drifted | `Cannot enable reconstructed … seam; upstream anchor changed` |

## Next

<CardGroup>
  <Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
    `GROK_BOT_018_APP` vs cache vs LFS DMG vs public URL, plus SHA-256 gates.
  </Card>
  <Card title="Choose an inference provider" href="/choose-inference-provider">
    Persist `inferenceProvider` and satisfy Cursor, Claude Code, Codex, or `OPENROUTER_API_KEY`.
  </Card>
  <Card title="Package the macOS app" href="/package-macos-app">
    Output name, updater guard injection, and reconstructed identity.
  </Card>
  <Card title="Binding and renderer overrides" href="/edit-frontend-workspace">
    `GROK_BOT_RENDERER_SOURCE` versus the patched shipped renderer.
  </Card>
  <Card title="Verify and smoke" href="/verify-and-smoke">
    Isolated `SAND_DISABLE_*` and the production env denylist.
  </Card>
  <Card title="Router failures" href="/router-failures">
    Missing keys, private `auth.json`, and Claude Code install errors.
  </Card>
</CardGroup>

---

## 17. Desktop RPC

> window.desktop.agent getInferenceRouter, setInferenceRouter, getBoxRuntime, setBoxRuntime, and secrets upsert/list channels.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/17-desktop-rpc.md
- Generated: 2026-08-24T05:56:38.137Z

### Source Files

- `source/shared/rpc/main.ts`
- `source/electron-main/main-edge.ts`
- `source/electron-preload/preload.ts`
- `source/electron-preload/main-rpc-runtime.ts`
- `source/shared/rpc/coordinator.ts`
- `source/electron-main/secrets/secrets-ipc.ts`

---
title: "Desktop RPC"
description: "window.desktop.agent getInferenceRouter, setInferenceRouter, getBoxRuntime, setBoxRuntime, and secrets upsert/list channels."
---

`window.desktop` is the context-bridge object the primary preload exposes into the renderer. Settings → Router calls `window.desktop.agent.getInferenceRouter`, `setInferenceRouter`, `getBoxRuntime`, and `setBoxRuntime` over the `main` RPC edge (`sand-rpc:main:m:<method>`). Secrets stay off that edge: `window.desktop.secrets.list` / `upsert` / `reveal` / `remove` invoke `sand:secrets-*` IPC, even though `MAIN_METHOD_TABLE` still lists `listSecrets` and `upsertSecrets` as unserved names.

## Transport split

Preload installs two renderer globals: `desktop` (this page) and `coordinatorPort` (MessagePort to the node-agent coordinator). Do not send router, box-runtime, or user-secret mutations through `coordinatorPort`.

```mermaid
flowchart TB
  subgraph renderer["Renderer"]
    Patch["Settings Router patch"]
    Desktop["window.desktop"]
  end
  subgraph preload["dist/electron-preload/preload.cjs"]
    Bridge["createDesktopPreloadBridge"]
    Edge["bridgeRpcEdge(main)"]
  end
  subgraph mainProc["Electron main"]
    Serve["serveEdge trust=appWindow"]
    Handlers["createMainEdgeHandlers"]
    SecretsIpc["registerSecretsIpc"]
    Settings["SandSettingsStore"]
    Docker["startLocalDockerBox / stopLocalDockerBox"]
    UserSecrets["SandUserSecretsStore"]
    Push["createBoxSecretsPush"]
  end
  Patch --> Desktop
  Desktop --> Bridge
  Bridge -->|"sand-rpc:main:m:*"| Edge
  Edge --> Serve
  Serve --> Handlers
  Handlers --> Settings
  Handlers --> Docker
  Bridge -->|"sand:secrets-*"| SecretsIpc
  SecretsIpc --> UserSecrets
  SecretsIpc --> Push
```

| Surface | Renderer API | Wire | Served by |
|---|---|---|---|
| Inference router | `window.desktop.agent.getInferenceRouter` / `setInferenceRouter` | `sand-rpc:main:m:getInferenceRouter`, `sand-rpc:main:m:setInferenceRouter` | `createMainEdgeHandlers` |
| Box runtime | `window.desktop.agent.getBoxRuntime` / `setBoxRuntime` | `sand-rpc:main:m:getBoxRuntime`, `sand-rpc:main:m:setBoxRuntime` | `createMainEdgeHandlers` |
| Secrets | `window.desktop.secrets.list` / `reveal` / `upsert` / `remove` | `sand:secrets-list`, `sand:secrets-reveal`, `sand:secrets-upsert`, `sand:secrets-delete` | `registerSecretsIpc` |
| Unserved main names | Do not call `edge("listSecrets")` / `upsertSecrets` | Same `sand-rpc:main:m:*` family | Throws `main/unserved-method` |

Contract constants:

- Edge name: `main` (`MAIN_RPC_CONTRACT_NAME`)
- Method channel: `sand-rpc:${edge}:m:${method}`
- Event channel: `sand-rpc:${edge}:e:${event}`
- Args: `get*` methods send `{}`; `setInferenceRouter` / `setBoxRuntime` send a single object

<Note>
`MAIN_METHOD_TABLE` still includes `listSecrets`, `revealSecret`, `upsertSecrets`, and `removeSecrets` so the shipped method registry stays complete. Those four handlers are `unserved()` and fail with `main/unserved-method` / `This method still rides its hand-wired preload channel.`
</Note>

## Trust

Every served main-edge method, including the four router/runtime methods, uses trust policy `appWindow`:

- Test: `sender.isAppWindowTopFrame === true`
- Denial: `The main edge is only accessible from the Sand app window's top frame.`
- Failure envelope: `{ ok: false, failure: { code: "edge/untrusted-sender", detail: "<denial>" } }`

Secrets and client-persistence IPC require the sender and sender frame to match the trusted app-window contents and main frame.

- Secrets denial: `Secrets are only accessible from the Sand app window.` (`UntrustedSecretsSenderError`)
- Client persistence denial: `Client persistence is only accessible from the Sand app window.`

## Reply envelope

`bridgeRpcEdge` unwraps `{ ok: true, value }` or throws `EdgeCallFailure` from `{ ok: false, failure: { code, detail } }`.

| Code | When |
|---|---|
| `edge/untrusted-sender` | Sender is not the app window top frame |
| `edge/unknown-method` | IPC invoke itself failed |
| `edge/handler-failed` | Handler threw a non-`EdgeCallFailure` (includes `Unknown inference provider.` and `Unknown box runtime.`) |
| `main/unserved-method` | Method is registered but still on a hand-wired channel |
| `edge/handler-failed` with `The edge replied outside its envelope.` | Reply was not `{ ok, ... }` |

## Inference router

Preload wrappers:

```ts
getInferenceRouter: () => edge("getInferenceRouter")
setInferenceRouter: (provider: string) => edge("setInferenceRouter", { provider })
```

Valid `SandInferenceProvider` ids: `cursor`, `claude-code`, `codex`, `openrouter`. Default stored value is `cursor`.

:::endpoint INVOKE sand-rpc:main:m:getInferenceRouter Read persisted provider, usage, and local CLI status
Get does not take arguments. It reads `settingsStore.getInferenceProvider()`, then tries `readHostSettingsFromBox()` for usage. A host-settings failure is swallowed; usage falls back to `settingsStore.getInferenceRouterUsage()`. An unknown stored provider becomes `"cursor"`.

<ResponseField name="provider" type="SandInferenceProvider">
`cursor` \| `claude-code` \| `codex` \| `openrouter`
</ResponseField>
<ResponseField name="usage" type="SandInferenceRouterUsage | null">
`schemaVersion: 1` plus per-provider `{ requests, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, lastUsedAt }`. `lastUsedAt` is an ISO string or `null`.
</ResponseField>
<ResponseField name="local" type="object">
Local CLI probe for `codex` and `claude-code` only: `{ installed, authenticated, executablePath }`.
</ResponseField>
:::

:::endpoint INVOKE sand-rpc:main:m:setInferenceRouter Persist provider and echo usage plus local status
<ParamField body="provider" type="SandInferenceProvider" required>
Must pass `isSandInferenceProvider`. Anything else throws `Unknown inference provider.`
</ParamField>

Handler sequence:

1. `settingsStore.setInferenceProvider(provider)` (atomic `settings.json` persist)
2. `syncHostSettingsToBox({ inferenceProvider: provider })` — failure is caught and treated as `null`
3. Return `{ provider, usage, local }` with usage from the host result, else the local store

This call does **not** roll back the local persist if the box is unreachable.
:::

<RequestExample>
```json title="setInferenceRouter payload"
{ "provider": "openrouter" }
```
</RequestExample>

<ResponseExample>
```json title="getInferenceRouter / setInferenceRouter value"
{
  "provider": "openrouter",
  "usage": {
    "schemaVersion": 1,
    "providers": {
      "cursor": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "claude-code": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "codex": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null },
      "openrouter": { "requests": 0, "inputTokens": 0, "outputTokens": 0, "cacheReadTokens": 0, "cacheWriteTokens": 0, "lastUsedAt": null }
    }
  },
  "local": {
    "codex": { "installed": false, "authenticated": false, "executablePath": null },
    "claude-code": { "installed": false, "authenticated": false, "executablePath": null }
  }
}
```
</ResponseExample>

`local` meaning:

| Key | `installed` | `authenticated` |
|---|---|---|
| `codex` | `CODEX_HOME`/`~/.codex` `auth.json` exists | File is a regular file, mode has no group/other bits, `auth_mode === "chatgpt"`, and access/refresh/id tokens plus `account_id` are non-empty |
| `claude-code` | `CLAUDE_CODE_PATH` or a `claude` binary on disk | `~/.claude/.credentials.json` exists **or** `ANTHROPIC_API_KEY` is non-empty |

The packaged Settings panel listens for `sand-router-provider-changed` after a successful `setInferenceRouter`. That custom event is renderer-local; main does not broadcast a router-changed RPC event.

## Box runtime

Preload wrappers:

```ts
getBoxRuntime: () => edge("getBoxRuntime")
setBoxRuntime: (mode: string) => edge("setBoxRuntime", { mode })
```

Valid `SandBoxRuntime` values: `remote` (default) and `local-docker`.

:::endpoint INVOKE sand-rpc:main:m:getBoxRuntime Read mode plus Docker VM status
Unknown stored mode throws `Unknown box runtime.` Status always comes from `getLocalDockerStatus(settingsStore.settingsPath)`, including when mode is `remote`.
:::

:::endpoint INVOKE sand-rpc:main:m:setBoxRuntime Persist mode, start or stop the VM, restart coordinator
<ParamField body="mode" type="SandBoxRuntime" required>
`remote` or `local-docker`. Invalid values throw `Unknown box runtime.`
</ParamField>

Handler sequence:

1. Persist `boxRuntime` immediately
2. `local-docker` → `startLocalDockerBox(settingsPath)`; `remote` → `stopLocalDockerBox()`
3. On start/stop failure, persist the opposite mode and rethrow (coordinator is not restarted)
4. On success, `boxRecovery.restartCoordinator()`
5. Return `{ mode, status }` from a fresh `getLocalDockerStatus`
:::

<ResponseField name="mode" type="SandBoxRuntime">
The persisted runtime after a successful switch.
</ResponseField>
<ResponseField name="status.available" type="boolean">
Docker daemon answered `docker info`.
</ResponseField>
<ResponseField name="status.running" type="boolean">
Container `grok-bot-local-vm` is running.
</ResponseField>
<ResponseField name="status.ready" type="boolean">
Owned container is running and `http://127.0.0.1:1340/health` succeeds with the local token.
</ResponseField>
<ResponseField name="status.containerName" type="string">
Always `grok-bot-local-vm`.
</ResponseField>
<ResponseField name="status.image" type="string">
Pinned image `public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest`, or the inspected image if the container exists.
</ResponseField>
<ResponseField name="status.detail" type="string">
Human status string shown under Settings → Router → Use local Docker VM.
</ResponseField>

Typical `status.detail` values:

| Condition | `detail` |
|---|---|
| Docker missing / not running | daemon stderr, or `Docker is not running.` |
| No container | `Ready to create the local VM.` |
| Name taken by another owner | `Container grok-bot-local-vm exists but is not owned by Grok Bot.` |
| Gateway healthy | `Local Docker VM is ready.` |
| Container up, gateway not yet | `Container is starting.` |
| Owned, not running | `Local Docker VM is stopped.` |

Start failures that roll `boxRuntime` back to `remote` include Docker unavailable, an unowned `grok-bot-local-vm`, unexpected image, missing reconstructed `host-main.cjs` / box-exec daemon, create/start errors, and gateway not ready within three minutes.

## Secrets channels

Preload:

```ts
secrets: {
  list: () => ipc.invoke("sand:secrets-list"),
  reveal: (key: string) => ipc.invoke("sand:secrets-reveal", { key }),
  upsert: (entries: Record<string, string>) => ipc.invoke("sand:secrets-upsert", { entries }),
  remove: (keys: readonly string[]) => ipc.invoke("sand:secrets-delete", { keys }),
}
```

Settings → Router stores the OpenRouter key as `OPENROUTER_API_KEY` via `secrets.upsert`. Cursor, Claude Code, and Codex do not write this store for auth; they use account or local CLI status from `getInferenceRouter().local`.

:::endpoint INVOKE sand:secrets-list List secret keys
Returns `{ keys: string[], isPersistent: boolean }`. `keys` is the sorted union of encrypted disk keys and in-memory session keys for the current account slot. `isPersistent` is Electron `safeStorage` availability.
:::

:::endpoint INVOKE sand:secrets-reveal Decrypt one secret
<ParamField body="key" type="string" required>
Non-string `key` returns `null`.
</ParamField>
Returns the plaintext string, or `null` if missing / decrypt fails / secure storage is unavailable for disk values.
:::

:::endpoint INVOKE sand:secrets-upsert Merge entries and push to the box
<ParamField body="entries" type="Record<string, string>" required>
`parseSecretEntries` keeps only string values. Non-objects become `{}`.
</ParamField>
Requires a signed-in account scope (`SandSecretsAccountRequiredError`: `Box secrets can only change while an account is signed in`). Validates the merged map with `validateBoxSecrets`, encrypts with `safeStorage` when available (otherwise session-only), then `pushBoxSecrets()`. Return: `{ synced: boolean }`.
:::

:::endpoint INVOKE sand:secrets-delete Remove keys and push
<ParamField body="keys" type="string[]" required>
Non-string array elements are dropped.
</ParamField>
Same account-scope requirement as upsert. Return: `{ synced: boolean }`.
:::

`synced` is `true` only when `setBoxSecrets` on the host succeeds. Keychain lock (`OS secure storage is unavailable`) or host unreachable records telemetry and returns `synced: false` without throwing from the IPC handler (the upsert/remove itself already completed locally unless validation failed).

Disk file: `user-secrets.json` under Electron `app.getPath("userData")`, written `0o600`, schema `{ version: 2, accounts: { [accountSlot]: { [key]: base64Ciphertext } } }`. Legacy `{ version: 1, secrets }` migrates into the current account slot from `legacy`.

`validateBoxSecrets` constraints:

| Limit | Value |
|---|---|
| Max keys | `100` |
| Max value length | `32768` |
| Max combined key+value length | `98304` |
| Key pattern | `^[A-Za-z_][A-Za-z0-9_]*$` |
| Reserved exact names | `PATH`, `HOME`, `USER`, `SHELL`, `TERM`, `PWD`, `DISPLAY`, `CLOUD_AGENT_INJECTED_SECRET_NAMES` |
| Reserved prefixes | `SAND_`, `__CURSOR`, `LD_` |
| Reserved substring | names matching `/CURSOR_SANDBOX/i` |

Quit quiesces box secret pushes (`Box secrets pushes are quiesced for quit`).

## Adjacent `desktop.agent` IPC

`window.desktop.agent.clientPersistence` is a separate trusted-sender family, not the secrets store:

| Method | Channel |
|---|---|
| `read(key)` | `sand:client-persistence-read` |
| `write(key, value)` | `sand:client-persistence-write` |
| `remove(key)` | `sand:client-persistence-remove` |
| `listKeys(prefix)` | `sand:client-persistence-list-keys` |
| `migrateFromLocalStorage(entries)` | `sand:client-persistence-migrate` |

Write requires string `key` and `value` or throws `client persistence: write needs a string key and value`. Files are written `0o600`.

## Packaged Settings consumer

`npm run package` patches the shipped renderer so Settings → Router calls these APIs. The frontend Vite workspace is not the packaged UI unless `GROK_BOT_RENDERER_SOURCE` overrides it.

<Tabs>
<Tab title="Inference provider">
Load: `window.desktop.agent.getInferenceRouter()`. Save: `setInferenceRouter(id)` then `CustomEvent("sand-router-provider-changed")`. Errors surface as `error: String(message)`.
</Tab>
<Tab title="OpenRouter key">
Load keys: `window.desktop.secrets.list()` → `keys`. Save: `window.desktop.secrets.upsert({ OPENROUTER_API_KEY: trimmed })`. Empty input is a no-op.
</Tab>
<Tab title="Local Docker VM">
Load: `getBoxRuntime()`. Toggle sends the opposite of current `mode`. Failure restores the previous `mode` in UI state from the thrown `EdgeCallFailure` detail.
</Tab>
</Tabs>

`tests/publication-packaging.test.mjs` asserts the preload wrappers and the patched `desktop.agent.getInferenceRouter()` / `setInferenceRouter` / `getBoxRuntime` / `setBoxRuntime` call sites.

## Failures

| Symptom | Cause | What to do |
|---|---|---|
| `Unknown inference provider.` | `provider` not in the four ids | Send `cursor`, `claude-code`, `codex`, or `openrouter` |
| `Unknown box runtime.` | `mode` not `remote` or `local-docker` | Send one of those two strings |
| `main/unserved-method` on secrets | Renderer called the main-edge names | Use `window.desktop.secrets.*` |
| `edge/untrusted-sender` | Call did not originate in the app window top frame | Keep calls on the primary BrowserWindow renderer |
| `Box secrets can only change while an account is signed in` | No account scope | Sign in before upsert/remove |
| `synced: false` after upsert | Keychain locked or host unreachable | Check OS secure storage; retry when the box is up |
| Local Docker toggle reverts | `startLocalDockerBox` / `stopLocalDockerBox` threw | See Router and sandbox failures; `boxRuntime` is already reverted on disk |
| OpenRouter option shows "Paste API key" after save | `list()` keys do not include `OPENROUTER_API_KEY` | Confirm upsert did not throw validation/account errors |

## Next

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, default `cursor`, transcript schemaVersion 2, usage schemaVersion 1.
</Card>
<Card title="Box runtime" href="/box-runtime">
`remote` versus `local-docker`, coordinator restart, loopback gateway ownership.
</Card>
<Card title="Settings schema" href="/settings-schema">
`settings.json` version 1 fields including `inferenceProvider` and `boxRuntime`.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Settings → Router persist path and per-provider auth prerequisites.
</Card>
<Card title="Enable the local Docker sandbox" href="/enable-local-docker">
Toggle Use local Docker VM, health on `127.0.0.1:1340`, rollback on start failure.
</Card>
<Card title="Route Grok Bot plugin tools" href="/route-mcp-tools">
Coordinator-port `listRoutedMcpTools` / `executeRoutedMcpTool` (not `window.desktop`).
</Card>
<Card title="Router and sandbox failures" href="/router-failures">
Unknown provider, Codex auth.json, Claude Code missing, OpenRouter key, unowned VM.
</Card>
</CardGroup>

---

## 18. Local Docker connector

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

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/18-local-docker-connector.md
- Generated: 2026-08-24T05:57:01.336Z

### 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>

---

## 19. Provider clients

> Cursor default path, Claude Agent SDK query, Codex Responses at chatgpt.com/backend-api/codex/responses, OpenRouter OpenAI-compatible model default openai/gpt-5.2.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/19-provider-clients.md
- Generated: 2026-08-24T05:56:59.642Z

### Source Files

- `source/host/extensions/inference/provider-session.ts`
- `source/host/extensions/inference/codex-direct-responses.ts`
- `source/shared/node/inference-router-local.ts`
- `source/host/extensions/inference/cursor-session.ts`
- `tests/codex-direct-responses.test.mjs`
- `tests/inference-router-transcript.test.mjs`

---
title: "Provider clients"
description: "Cursor default path, Claude Agent SDK query, Codex Responses at chatgpt.com/backend-api/codex/responses, OpenRouter OpenAI-compatible model default openai/gpt-5.2."
---

`createHostInference` and `createCoordinatorInferenceRouter` both read `inferenceProvider` from `settings.json` (default `cursor`). Cursor turns stay on the signed-in Connect RPC `InferenceService` at `https://api2.cursor.sh`. Non-cursor turns go through `runRoutedProviderText` / `createProviderPromptSession` in `provider-session.ts`: Claude Agent SDK `query`, Codex SSE `POST https://chatgpt.com/backend-api/codex/responses`, or OpenRouter `https://openrouter.ai/api/v1` with default model `openai/gpt-5.2`.

<Note>
Routed chat with Grok Bot plugins is the coordinator `sendPrompt` path. Host prompt sessions (`createProviderPromptSession`) stream text and record usage but do not attach an MCP executor.
</Note>

## Provider matrix

| `SandInferenceProvider` | Transport | Auth | Default model id | Plugin tools |
| --- | --- | --- | --- | --- |
| `cursor` | Connect RPC `aiserver.v1.InferenceService` | Grok Bot / Cursor access token | `grok-4.5` (`maxMode: true`, `effort=high`, `fast=true`) | Native host tools (not the routed MCP loop) |
| `claude-code` | `@anthropic-ai/claude-agent-sdk` `query` | Local Claude Code CLI + `~/.claude/.credentials.json` or `ANTHROPIC_API_KEY` | `claude-code` (`SAND_CLAUDE_MODEL` optional) | HTTP MCP bridge `grok_bot_plugins` |
| `codex` | Direct Responses SSE | Private `auth.json` ChatGPT tokens (CLI binary is **not** on the request path) | `gpt-5.4` | Direct `executeRoutedMcpTool` |
| `openrouter` | Vercel AI SDK `streamText` + `@ai-sdk/openai` | `OPENROUTER_API_KEY` env or `box-secrets.json` | `openai/gpt-5.2` | Direct `executeRoutedMcpTool` |

`SAND_INFERENCE_PROVIDERS` is the closed set `cursor`, `claude-code`, `codex`, `openrouter`. Unknown values fail `isSandInferenceProvider` and desktop `setInferenceRouter`.

```mermaid
flowchart TB
  subgraph ui [Settings Router]
    settings["settings.json inferenceProvider"]
    secrets["box-secrets.json OPENROUTER_API_KEY"]
  end
  subgraph coord [node-agent-coordinator]
    dispatch["createCoordinatorInferenceRouter.dispatch"]
    sendPrompt["sendPrompt when provider != cursor"]
    mcpBridge["createRoutedMcpBridge 127.0.0.1 MCP HTTP"]
    runText["runRoutedProviderText"]
  end
  subgraph host [host inference]
    createHost["createHostInference.createSession"]
    cursorSess["createCursorInferencePromptSession"]
    providerSess["createProviderPromptSession"]
  end
  subgraph clients [Provider clients]
    claudeQ["queryClaude Claude Agent SDK"]
    codexSSE["streamCodexDirectResponses"]
    orChat["createOpenAI OpenRouter chat"]
  end
  subgraph external [External endpoints]
    cursorApi["https://api2.cursor.sh"]
    claudeBin["claude executable"]
    codexApi["https://chatgpt.com/backend-api/codex/responses"]
    orApi["https://openrouter.ai/api/v1"]
    oauth["https://auth.openai.com/oauth/token"]
  end
  settings --> dispatch
  settings --> createHost
  dispatch --> sendPrompt
  sendPrompt --> runText
  runText --> claudeQ
  runText --> codexSSE
  runText --> orChat
  sendPrompt --> mcpBridge
  mcpBridge --> claudeQ
  createHost -->|cursor| cursorSess
  createHost -->|claude-code / codex / openrouter| providerSess
  cursorSess --> cursorApi
  claudeQ --> claudeBin
  codexSSE --> codexApi
  codexSSE -->|401 refresh| oauth
  orChat --> orApi
  secrets --> orChat
```

## Shared routed contract

All three non-cursor clients inject the same system instructions (`GROK_ROUTER_SYSTEM_PROMPT`): the model is Grok Bot, not Codex CLI or Claude Code, and must use already-connected plugin tools instead of asking for keys.

Tool-loop limits:

| Path | No tools | With tools |
| --- | --- | --- |
| Codex `maxSteps` | `1` | `8` |
| OpenRouter `streamText` `maxSteps` | `1` | `8` |
| Claude Code `maxTurns` | `1` | `8` |

Successful usage is written through `SandSettingsStore.recordInferenceUsage` into `inferenceRouterUsage` (`schemaVersion: 1`): `requests`, `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `lastUsedAt`.

Coordinator turns also persist `inference-router-transcript.json` (`schemaVersion: 2`, last 200 entries per agent). The first assistant delta is delayed **1200 ms** so the shipped transcript can show composing state.

## Cursor

Default route. `createCursorSandInference` / `createCursorInferencePromptSession` build a Connect client with `httpVersion: "1.1"` against `getConfiguredBackendUrl()`.

<ParamField body="SAND_BACKEND_URL" type="string">
Overrides the Cursor backend base URL. Falls back to `CURSOR_API_BASE_URL`, then `https://api2.cursor.sh`.
</ParamField>

<ParamField body="SAND_AGENT_MODEL" type="string">
Overrides the requested `modelId` for a Cursor session.
</ParamField>

<ParamField body="SAND_AGENT_MOCK_RESPONSE" type="string">
When set, skips the network: either a JSON mock script (`sendMessage` / `toolCalls`) or a chunked plain-text mock.
</ParamField>

Request headers on the Cursor transport:

| Header | Value |
| --- | --- |
| `authorization` | `Bearer <accessToken>` (deleted for `authMode: "anonymous"`) |
| `x-cursor-checksum` | `createCursorChecksum(machineId)` |
| `x-cursor-client-type` | `sand` |
| `x-cursor-client-version` | Stamped app version, with `-dev` / `-lab` suffixes from box namespace |
| `x-sand-box-namespace` | `dev` \| `lab` \| `prod` |
| `x-ghost-mode` | Derived from privacy-mode lookup; Dashboard `GetUserPrivacyMode` always sends `"true"` |
| `x-request-id` | Existing header or new UUID |
| `x-parent-request-id` / `x-root-parent-request-id` | Optional request lineage |

Default model selection is `{ modelId: "grok-4.5", maxMode: true, parameters: [{ id: "effort", value: "high" }, { id: "fast", value: "true" }] }`. Computer-use subagents default to `claude-opus-4-8`. Summarization, computer-use, and `skipLabeling` sessions skip follow-up labeling.

`sendPrompt` is **not** intercepted when the provider is `cursor`; the remote host transcript remains authoritative.

## Claude Code

`claudeExecutor` resolves a `claude` binary, then runs `query` from `@anthropic-ai/claude-agent-sdk`.

Executable search order (`resolveClaudeCodeCliPath`):

1. `CLAUDE_CODE_PATH`
2. `~/.local/bin/claude`
3. `~/.claude/local/claude`
4. `$PATH` entries named `claude`
5. `/opt/homebrew/bin/claude`
6. `/usr/local/bin/claude`

If none exist: `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.`

Local status (`getLocalInferenceCliStatus()["claude-code"]`):

| Field | Meaning |
| --- | --- |
| `installed` | Executable found |
| `authenticated` | `~/.claude/.credentials.json` exists **or** `ANTHROPIC_API_KEY` is non-empty |
| `executablePath` | Resolved binary |

`query` options:

| Option | Value |
| --- | --- |
| `pathToClaudeCodeExecutable` | Resolved CLI |
| `cwd` | Sand root (`getSandRootDir()`) |
| `permissionMode` | `"default"` |
| `persistSession` | `false` |
| `tools` | `[]` without MCP; `["mcp__grok_bot_plugins__*"]` with MCP |
| `mcpServers.grok_bot_plugins` | `{ type: "http", url: mcpServerUrl }` |
| `strictMcpConfig` | `true` when MCP is attached |
| `maxTurns` | `1` or `8` |
| `model` | `SAND_CLAUDE_MODEL` when non-empty |

The coordinator attaches `createRoutedMcpBridge`: a loopback HTTP JSON-RPC server at `http://127.0.0.1:<ephemeral>/mcp/<secret>` (`protocolVersion: "2025-03-26"`, `serverInfo.name: "grok-bot-plugins"`). `tools/list` and `tools/call` proxy `listRoutedMcpTools` / `executeRoutedMcpTool`. Body size cap is 1 MiB.

A result message with `subtype !== "success"` throws joined `errors` or `Claude Code failed (<subtype>).`. Empty stream: `Claude Code ended without a result.` Provider metadata stores `{ anthropic: { sessionId, totalCostUsd } }`. The prompt-session `modelId` is the literal `"claude-code"`.

## Codex

Grok Bot does **not** spawn the Codex CLI for inference. `getLocalInferenceCliStatus().codex` still reports the binary for Settings status, but comments and code treat login as an HTTP credential store.

### Credentials

Path: `$CODEX_HOME/auth.json` or `~/.codex/auth.json`.

The file must be a regular file, not a symlink, with no group/other permission bits (`mode & 0o077 === 0`). Required fields:

- `auth_mode === "chatgpt"`
- `tokens.access_token`, `tokens.refresh_token`, `tokens.id_token`, `tokens.account_id` (all non-empty strings)

Otherwise: `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.` Permission failures: `Codex login credentials must be a private direct regular file.`

On HTTP `401`, `refreshCodexCredentials` POSTs `grant_type=refresh_token` to `https://auth.openai.com/oauth/token` with `client_id` from the id-token JWT `aud`. The rewrite is atomic: `0o600` tempfile + `rename`. Retry uses `authorization: Bearer <accessToken>` and `ChatGPT-Account-Id`.

### Model

<ParamField body="SAND_CODEX_MODEL" type="string">
Overrides the Responses `model`. Else first `model = "..."` in `$CODEX_HOME/config.toml` / `~/.codex/config.toml`. Default `gpt-5.4`.
</ParamField>

<ParamField body="SAND_CODEX_REASONING_EFFORT" type='"minimal" \| "low" \| "medium" \| "high" \| "xhigh"'>
Overrides reasoning effort. Else `model_reasoning_effort` in `config.toml`. Omitted when unset.
</ParamField>

`resolveCodexCliPath` (status only): `CODEX_PATH`, `~/.local/bin/codex`, `~/.codex/bin/codex`, `$PATH`, `/opt/homebrew/bin/codex`, `/usr/local/bin/codex`. `installed` is true when `auth.json` exists, not when the binary exists.

### Responses transport

`streamCodexDirectResponses` owns the SSE reader. Default `maxSteps` is `8`.

:::endpoint POST https://chatgpt.com/backend-api/codex/responses
Stream one Codex Responses turn. Grok Bot sets `user-agent: grok-bot-router/1`, `accept: text/event-stream`, `content-type: application/json`, plus the ChatGPT bearer headers.

**Body**

- `model` — configured Codex model
- `instructions` — `GROK_ROUTER_SYSTEM_PROMPT`
- `input` — conversation roles coerced to `user` | `assistant`
- `tools` — `{ type: "function", name, description?, parameters, strict: false }` when tools exist
- `tool_choice` — `"auto"` when tools exist
- `parallel_tool_calls` — `true` when tools exist
- `reasoning` — `{ effort, summary: "auto" }` when effort is configured
- `include` — `["reasoning.encrypted_content"]`
- `stream` — `true`
- `store` — `false`

**SSE events consumed**

- `response.output_text.delta` → `{ type: "text-delta", delta }`
- `response.output_item.done` — captured for tool-call fallback when `response.output` is empty
- `response.completed` — required; missing it throws `Codex direct response ended without response.completed.`
- `response.failed` / `error` — fail closed

**Tool continuation**

Function calls keep the provider `call_id`. Outputs are `{ type: "function_call_output", call_id, output }` JSON. Unknown tools and executor exceptions return `{ isError: true, error }` instead of aborting the loop. Exceeding `maxSteps` throws `Codex exceeded Grok Bot's N-step tool limit.` Truncated SSE throws `Codex direct response ended with an incomplete SSE event.`
:::

Usage maps `input_tokens`, `output_tokens`, and `input_tokens_details.cached_tokens` (`cacheWriteTokens` is always `0`). Provider metadata is `{ openai: { responseId, direct: true } }`.

```mermaid
sequenceDiagram
  participant Coord as Coordinator sendPrompt
  participant Exec as codexExecutor
  participant API as chatgpt.com/.../codex/responses
  participant Auth as auth.openai.com/oauth/token
  participant MCP as executeRoutedMcpTool
  Coord->>Exec: runRoutedProviderText(codex, messages, tools)
  Exec->>API: POST stream store=false
  alt HTTP 401
    Exec->>Auth: refresh_token
    Exec->>API: retry with new Bearer
  end
  API-->>Exec: response.output_text.delta / function_call
  Exec->>MCP: executeTool(source, args, call_id)
  MCP-->>Exec: tool result JSON
  Exec->>API: input plus function_call_output
  API-->>Exec: response.completed
  Exec-->>Coord: done text + usage
```

## OpenRouter

`openRouterExecutor` builds `createOpenAI({ apiKey, baseURL: "https://openrouter.ai/api/v1", compatibility: "compatible", name: "openrouter" })`.

<ParamField body="OPENROUTER_API_KEY" type="string" required>
Read from the process environment first, then `box-secrets.json` `secrets.OPENROUTER_API_KEY`. Missing: `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.`
</ParamField>

<ParamField body="SAND_OPENROUTER_MODEL" type="string">
OpenRouter model id. Default `openai/gpt-5.2`.
</ParamField>

Fixed client headers:

| Header | Value |
| --- | --- |
| `HTTP-Referer` | `https://github.com/grok-bot-reconstructed` |
| `X-Title` | `Grok Bot Reconstructed` |

`streamText` uses the shared system prompt, `toolCallStreaming: true`, and JSON-schema tools from `inputSchema` or `parameters`. Cache token fields are recorded as `0`. Settings UI upserts the key through `window.desktop.secrets.upsert({ OPENROUTER_API_KEY })`.

## Coordinator vs host session

<Tabs>
<Tab title="Chat sendPrompt">
When `inferenceProvider` is not `cursor`, `createCoordinatorInferenceRouter.dispatch("sendPrompt")` queues per `agentId`, appends a local user row (`t{turn}u`), waits 1200 ms, then:

- **claude-code** — `mcpServerUrl` from the loopback MCP bridge
- **codex / openrouter** — `listRoutedMcpTools` plus `executeRoutedMcpTool` (`providerIdentifier`, `name`, `toolName`, `args`, `toolCallId`, `agentId`)

Failures append `Router error: <message>` as an assistant row instead of throwing to the renderer. Requires non-empty `agentId` and `prompt`.
</Tab>
<Tab title="Host createSession">
`createHostInference.createSession` (and summarization) uses Cursor Connect when the setting is `cursor`, otherwise `createProviderPromptSession`. The same routed split exists inside `createCursorSandInference` and `createCursorInferencePromptSession` so nested Cursor session builders cannot ignore the router. Host routed streams pass tool definitions without `executeTool` / MCP URL.
</Tab>
</Tabs>

## Environment and files

| Name | Used by |
| --- | --- |
| `OPENROUTER_API_KEY` | OpenRouter credential |
| `SAND_OPENROUTER_MODEL` | OpenRouter model (default `openai/gpt-5.2`) |
| `SAND_CLAUDE_MODEL` | Claude Agent SDK `model` |
| `CLAUDE_CODE_PATH` | Claude CLI override |
| `ANTHROPIC_API_KEY` | Claude status `authenticated` |
| `CODEX_HOME` | Codex `auth.json` / `config.toml` directory |
| `SAND_CODEX_MODEL` | Codex Responses model (default `gpt-5.4`) |
| `SAND_CODEX_REASONING_EFFORT` | Codex `reasoning.effort` |
| `CODEX_PATH` | Codex CLI path for Settings status only |
| `SAND_BACKEND_URL` / `CURSOR_API_BASE_URL` | Cursor Connect base URL |
| `SAND_AGENT_MODEL` | Cursor requested model |
| `SAND_AGENT_MOCK_RESPONSE` | Cursor mock executor |

Credential files:

| Path | Constraint |
| --- | --- |
| `$CODEX_HOME/auth.json` | Private regular file, `auth_mode: "chatgpt"` |
| `$CODEX_HOME/config.toml` | Optional `model` and `model_reasoning_effort` |
| `~/.claude/.credentials.json` | Claude authenticated flag |
| `<sandRoot>/box-secrets.json` | Persisted `OPENROUTER_API_KEY` |
| `<sandRoot>/settings.json` | `inferenceProvider`, `inferenceRouterUsage` |
| `<dataDir>/inference-router-transcript.json` | Routed chat store, mode `0o600` |

## Failure strings

| Client | Error |
| --- | --- |
| Claude Code | `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.` |
| Claude Code | `Claude Code ended without a result.` / `Claude Code failed (<subtype>).` |
| Codex | `Codex login credentials must be a private direct regular file.` |
| Codex | `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.` |
| Codex | `Codex login expired and could not be refreshed. Run \`codex login\` again.` |
| Codex | `Codex requested a tool but Grok Bot did not provide an executor.` |
| Codex | `Codex direct request failed (<status>: …).` |
| OpenRouter | `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.` |
| Coordinator | `Local inference routing requires an agentId and prompt` |
| Desktop RPC | `Unknown inference provider.` |

## Next

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, default `cursor`, transcript `schemaVersion` 2, usage `schemaVersion` 1.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Settings → Router, persist `inferenceProvider`, and satisfy each client’s login.
</Card>
<Card title="Route Grok Bot plugin tools" href="/route-mcp-tools">
Claude MCP bridge versus Codex/OpenRouter direct `executeRoutedMcpTool`.
</Card>
<Card title="Environment variables" href="/environment-variables">
`OPENROUTER_API_KEY`, `SAND_OPENROUTER_MODEL`, `CODEX_HOME`, `CLAUDE_CODE_PATH`, Cursor backend overrides.
</Card>
<Card title="Router and sandbox failures" href="/router-failures">
Unknown provider, private `auth.json`, missing Claude CLI, missing OpenRouter key.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
`getInferenceRouter` / `setInferenceRouter` and secrets upsert for the OpenRouter key.
</Card>
</CardGroup>

---

## 20. Bootstrap failures

> LFS pointer-sized archives, DMG checksum mismatch, missing app.asar files, GROK_BOT_018_APP version mismatch, and hydration checksum errors.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/20-bootstrap-failures.md
- Generated: 2026-08-24T05:57:11.310Z

### Source Files

- `scripts/bootstrap-runtime.mjs`
- `scripts/lib/runtime.mjs`
- `scripts/lib/config.mjs`
- `tests/publication-bootstrap.test.mjs`
- `tests/research-archives.test.mjs`
- `research-archives/README.md`

---
title: "Bootstrap failures"
description: "LFS pointer-sized archives, DMG checksum mismatch, missing app.asar files, GROK_BOT_018_APP version mismatch, and hydration checksum errors."
---

`npm run bootstrap` (`scripts/bootstrap-runtime.mjs`) is fail-closed. It selects one 0.18.0 macOS runtime, copies it to `.cache/runtime/Grok Bot.app`, then hydrates gitignored `src/app/dist` from `Contents/Resources/app.asar` only when both the DMG (when used) and the ASAR match the pins in `scripts/lib/config.mjs`. A bad Git LFS pointer, a wrong `GROK_BOT_018_APP` bundle, or a drifted `app.asar` aborts; the script does not skip to the next source.

## Pinned identities

| Artifact | Path | Bytes | SHA-256 |
| --- | --- | ---: | --- |
| macOS DMG | `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` | `155793020` | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| Cached DMG | `.cache/downloads/Grok_Bot_0.18.0.dmg` | same as pin | same as pin |
| Upstream `app.asar` | `Grok Bot.app/Contents/Resources/app.asar` | (hashed, not size-pinned) | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |

Public fallback URL (used only when the archived DMG is absent):

`https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg`

The preserved Windows `Grok_Bot_0.18.0_Setup.exe` is an evidence archive only. Bootstrap never mounts or hydrates it.

<ParamField body="GROK_BOT_018_APP" type="string">
Absolute path to an existing `Grok Bot.app`. When set and non-empty after trim, bootstrap copies that bundle with `/usr/bin/ditto` into `.cache/runtime/Grok Bot.app` and skips DMG attach. `CFBundleShortVersionString` must be exactly `0.18.0`.
</ParamField>

## Source selection

```mermaid
flowchart TD
  start["npm run bootstrap"] --> env{"GROK_BOT_018_APP set?"}
  env -->|yes| ditto["ditto into .cache/runtime/Grok Bot.app"]
  ditto --> validate["validateRuntimeApp"]
  env -->|no| cache{"cachedRuntimeApp exists?"}
  cache -->|yes| validate
  cache -->|no| dmg["downloadDmg"]
  dmg --> cachedDmg{".cache/downloads/Grok_Bot_0.18.0.dmg hash == dmgSha256?"}
  cachedDmg -->|yes| attach
  cachedDmg -->|no, file present| dropCache["rm cached DMG"]
  dropCache --> archive{"archived LFS DMG exists?"}
  cachedDmg -->|no file| archive
  archive -->|yes, hash mismatch| lfsFail["Archived DMG checksum mismatch — git lfs pull"]
  archive -->|yes, hash match| copyArchive["copyFile to cachedDmg"]
  archive -->|absent| fetch["fetch dmgUrl"]
  fetch -->|HTTP not ok or empty body| httpFail["Download failed: HTTP N"]
  fetch -->|hash mismatch| netHashFail["DMG checksum mismatch — delete .partial"]
  fetch -->|hash match| rename["rename .partial to cachedDmg"]
  copyArchive --> attach["hdiutil attach → Grok Bot.app"]
  rename --> attach
  attach --> validate
  validate -->|version != 0.18.0| verFail["Expected Grok Bot 0.18.0"]
  validate -->|missing executable or app.asar.unpacked| incomplete["Incomplete Grok Bot runtime"]
  validate -->|ok| hydrate["hydrateSourcePayloadFromAsar"]
  hydrate -->|asar hash mismatch| asarFail["Upstream app.asar checksum mismatch"]
  hydrate -->|missing required file| missingFail["Upstream app.asar is missing …"]
  hydrate -->|ok| ready["src/app/dist + Runtime ready"]
```

<Warning>
If `.cache/runtime/Grok Bot.app` already exists and `GROK_BOT_018_APP` is unset, bootstrap validates that cache and never opens the DMG. A stale or incomplete cache does not fall through to LFS or the public URL. Delete `.cache/runtime/Grok Bot.app` (and, if needed, `.cache/downloads/Grok_Bot_0.18.0.dmg`) before retrying.
</Warning>

`scripts/lib/build-asar.mjs` later calls `resolveRuntimeApp()`. Packaging without a successful bootstrap fails with `Missing 0.18.0 runtime. Run \`npm run bootstrap\` first.`

## Runtime gates

`validateRuntimeApp` requires all of:

| Check | Expected |
| --- | --- |
| `Contents/Info.plist` → `CFBundleShortVersionString` via `/usr/bin/plutil -extract … raw` | `0.18.0` (`upstreamVersion`) |
| `Contents/MacOS/Grok Bot` | regular file |
| `Contents/Resources/app.asar.unpacked` | directory |

DMG extract looks for `Grok Bot.app` at the `hdiutil` mountpoint (`-readonly -nobrowse`). Detach always runs in `finally`.

Hydration (`hydrateSourcePayloadFromAsar`) then:

1. SHA-256s the ASAR bytes against `upstreamAsarSha256` (overridable only by callers that pass `expectedSha256`; bootstrap does not override).
2. Extracts into a temp dir under `.cache/source-payloads/grok-bot-018-*`.
3. Requires these files inside the archive:

   - `dist/electron-main/main.cjs`
   - `dist/host/host-main.cjs`
   - `dist/renderer/index.html`

4. Replaces `src/app/dist` (`rm` then `cp` of the archive `dist/` tree only).
5. Deletes the temp extract in `finally`.

:::files
.cache/
  downloads/Grok_Bot_0.18.0.dmg
  downloads/Grok_Bot_0.18.0.dmg.partial   # network download; deleted on hash fail
  runtime/Grok Bot.app
  source-payloads/                        # temp extract; removed after hydrate
src/app/dist/                             # gitignored hydration output
  electron-main/main.cjs
  host/host-main.cjs
  renderer/index.html
research-archives/original/0.18.0/
  artifacts.json                          # schemaVersion 1 inventory
  SHA256SUMS
  macos-arm64/Grok_Bot_0.18.0.dmg         # Git LFS
:::

## Failure catalog

<AccordionGroup>
<Accordion title="LFS pointer instead of the 0.18.0 DMG">

Git tracks `research-archives/original/**/*.dmg` and `**/*.exe` with `filter=lfs`. A clone or source ZIP without `git lfs pull` leaves a small pointer file at the DMG path. `exists(archivedDmg)` is then true, so bootstrap **does not** fetch the public URL.

**Bootstrap error**

```text
Archived DMG checksum mismatch: expected a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb, got <pointer-hash>. Run git lfs pull before bootstrapping.
```

**Test error** (`tests/research-archives.test.mjs`)

`metadata.size === artifact.bytes` fails with `<path> requires git lfs pull` when the file is not `155793020` bytes (macOS) or `125825552` bytes (Windows). Hosting ZIP/tarball downloads can also ship pointer files unless the host includes LFS objects.

**Fix**

```sh
git lfs install
git lfs pull
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
npm run bootstrap
```

Confirm `git lfs ls-files` lists both installers and `ls -l research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` is not pointer-sized.

</Accordion>

<Accordion title="DMG checksum mismatch (cache or download)">

A cached DMG whose digest is not `dmgSha256` is deleted, then the archived copy or network fetch runs. A network payload is written to `Grok_Bot_0.18.0.dmg.partial` with mode `0o600`; a bad digest deletes the partial and throws. `fetch` uses `redirect: "follow"`.

**Errors**

```text
DMG checksum mismatch: expected a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb, got <digest>
Download failed: HTTP <status>
```

**Fix**

- Delete `.cache/downloads/Grok_Bot_0.18.0.dmg` and any `.partial`.
- Prefer a verified LFS archive over the network.
- Retry only after `shasum -a 256 -c SHA256SUMS` passes on the archived DMG.

There is no retry loop and no HTTP redirect-to-HTML tolerance: a non-ok response or null body fails immediately.

</Accordion>

<Accordion title="GROK_BOT_018_APP version or completeness mismatch">

`GROK_BOT_018_APP` must resolve to the upstream 0.18.0 app, not a reconstructed bundle under `dist/`. Version is read from Info.plist, not from `src/app/package.json`.

**Errors**

```text
Expected Grok Bot 0.18.0, got <version> at <appPath>
Incomplete Grok Bot runtime at <appPath>
/usr/bin/plutil exited with <code>: <stderr>
```

Incomplete means `Contents/MacOS/Grok Bot` is not a file or `Contents/Resources/app.asar.unpacked` is not a directory. A missing Info.plist surfaces as a `plutil` non-zero exit from `capture()`.

**Fix**

Point `GROK_BOT_018_APP` at a real 0.18.0 `Grok Bot.app` (from the pinned DMG), or unset it and bootstrap from LFS. Do not pass `dist/Grok Bot 0.18 Reconstructed.app`: that bundle can still report `0.18.0` and then fail the ASAR pin.

</Accordion>

<Accordion title="Missing or incomplete app.asar payload">

Hydration reads `Contents/Resources/app.asar`. After extract it requires the three `dist/` files above. `app.asar.unpacked` is a validate-time directory check, not an ASAR listing check.

**Errors**

```text
Upstream app.asar is missing dist/electron-main/main.cjs
Upstream app.asar is missing dist/host/host-main.cjs
Upstream app.asar is missing dist/renderer/index.html
```

A missing ASAR file itself fails at `readFile` (Node `ENOENT`), not a custom message. DMG attach that cannot find `Grok Bot.app` fails inside `cacheRuntimeFromApp` / `validateRuntimeApp`. `hdiutil` / `ditto` failures look like:

```text
/usr/bin/hdiutil exited with <code>
/usr/bin/ditto exited with <code>
```

**Fix**

Re-bootstrap from the pinned DMG. Do not hand-edit `src/app/dist`. Confirm the mounted app contains `Contents/Resources/app.asar` before retrying hydration.

</Accordion>

<Accordion title="Hydration checksum mismatch">

Every hydrate hashes the full ASAR. Bootstrap always expects `upstreamAsarSha256`. `tests/publication-bootstrap.test.mjs` asserts `hydrateSourcePayloadFromAsar` rejects `/checksum mismatch/` when `expectedSha256` is wrong.

**Error**

```text
Upstream app.asar checksum mismatch: expected 6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58, got <digest>
```

Typical causes: reconstructed or patched `app.asar` under `GROK_BOT_018_APP`, a non-0.18.0 build that still reports version `0.18.0`, or a truncated copy.

**Fix**

Use the DMG-extracted upstream app. Remove `.cache/runtime/Grok Bot.app` so the next run cannot reuse a bad cache. Successful hydrate prints the pinned digest.

</Accordion>
</AccordionGroup>

## Diagnose

<Steps>
<Step title="Confirm LFS objects, not pointers">

```sh
git lfs ls-files
ls -l research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
```

Expect `155793020` bytes and digest `a253ccd8…d203eb`. A ~100-byte text file is an LFS pointer.

</Step>
<Step title="Inspect env and cache">

```sh
printf '%s\n' "${GROK_BOT_018_APP-}"
ls -ld ".cache/runtime/Grok Bot.app" ".cache/downloads/Grok_Bot_0.18.0.dmg" 2>/dev/null || true
/usr/bin/plutil -extract CFBundleShortVersionString raw ".cache/runtime/Grok Bot.app/Contents/Info.plist"
```

Unset `GROK_BOT_018_APP` unless it is a verified 0.18.0 app. If the cached app exists, bootstrap will not re-read the DMG.

</Step>
<Step title="Clear the fail-closed cache and re-run">

```sh
rm -rf ".cache/runtime/Grok Bot.app" .cache/downloads/Grok_Bot_0.18.0.dmg .cache/downloads/Grok_Bot_0.18.0.dmg.partial
npm run bootstrap
```

</Step>
</Steps>

<Tabs>
<Tab title="LFS archive">

```sh
git lfs install
git lfs pull
npm run bootstrap
```

</Tab>
<Tab title="Existing app">

```sh
export GROK_BOT_018_APP="/path/to/Grok Bot.app"
npm run bootstrap
```

</Tab>
<Tab title="Public DMG">

Remove or rename the archived DMG only if you intend network fetch. Bootstrap copies a valid archive before `fetch`. After a failed download, delete `.partial` (the script already does this on hash mismatch).

</Tab>
</Tabs>

## Success signal

<ResponseExample>

```text
Runtime ready: <repo>/.cache/runtime/Grok Bot.app
Checksum-pinned source payload ready: <repo>/src/app/dist (6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58)
The checksum-pinned app supplies only the Electron shell, ABI-matched native dependencies, and explicitly documented build fallbacks.
```

</ResponseExample>

Also true after a good run:

- `src/app/dist/electron-main/main.cjs`, `src/app/dist/host/host-main.cjs`, and `src/app/dist/renderer/index.html` exist.
- `src/app/dist/` remains gitignored.
- `node --test tests/research-archives.test.mjs tests/publication-bootstrap.test.mjs` passes when LFS objects are present.

Do not weaken `dmgSha256`, `upstreamAsarSha256`, or the required ASAR file list to make bootstrap pass.

## Next

<CardGroup>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
Happy-path order: GROK_BOT_018_APP, cached app, LFS DMG, then the public URL.
</Card>
<Card title="Installation" href="/installation">
Git LFS, Node 26.5.x, and Xcode tools required before bootstrap.
</Card>
<Card title="Preserve original installers" href="/preserve-original-installers">
artifacts.json schemaVersion 1 and shasum -c SHA256SUMS.
</Card>
<Card title="Environment variables" href="/environment-variables">
GROK_BOT_018_APP and related overrides.
</Card>
<Card title="Packaging failures" href="/packaging-failures">
Errors after a successful hydrate, including Missing 0.18.0 runtime.
</Card>
<Card title="Evidence and provenance" href="/evidence-and-provenance">
Pinned 0.18.0 DMG and app.asar identities.
</Card>
</CardGroup>

---

## 21. Packaging failures

> Non-darwin package abort, missing renderer patch anchors, ad-hoc codesign retry, xattr quarantine, and verify required ASAR entries.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/21-packaging-failures.md
- Generated: 2026-08-24T05:57:34.833Z

### Source Files

- `scripts/package-macos.mjs`
- `scripts/lib/codesign.mjs`
- `scripts/lib/macos-package-verification.mjs`
- `scripts/lib/router-renderer-patch.mjs`
- `scripts/verify.mjs`
- `tests/publication-packaging.test.mjs`

---
title: "Packaging failures"
description: "Non-darwin package abort, missing renderer patch anchors, ad-hoc codesign retry, xattr quarantine, and verify required ASAR entries."
---

`npm run package` runs `npm run check` then `node scripts/package-macos.mjs`. That script is darwin-only: it builds the fidelity reconstructed ASAR (`buildFidelityReconstructedAsar`), copies the checksum-pinned 0.18 Electron shell with `/usr/bin/ditto`, replaces `Contents/Resources/app.asar` and `app.asar.unpacked`, rewrites reconstructed identity in `Info.plist`, strips extended attributes with `/usr/bin/xattr -cr`, ad-hoc signs with identity `-`, then verifies the signature and package invariant. Default output is `dist/Grok Bot 0.18 Reconstructed.app` unless `GROK_BOT_OUTPUT_APP_NAME` sets a different basename under `dist/`.

<Warning>
Packaging does not produce a Windows installer, a notarized Developer ID signature, or a clean-source renderer. The packaged UI is the shipped 0.18 renderer plus the exact Settings Router patch. Failures below are abort-the-build, not launch-time router errors.
</Warning>

## Abort order

| Gate | Owner | Typical abort |
| --- | --- | --- |
| `npm run check` | `package` script | Typecheck or `node --test tests/*.test.mjs` fails before packaging starts |
| `process.platform !== "darwin"` | `scripts/package-macos.mjs` | `The reconstructed macOS application can only be packaged on macOS.` |
| Settings chunk scan | `applyOriginalRendererRouterPatch` | `Expected one original Settings registry and panel chunk, found N/M.` |
| Exact string replace | `replaceExactlyOnce` | `Original renderer <label> anchor is missing or ambiguous.` |
| Official shell / ASAR hashes | `verifyOfficialMacReference` | `Official Mac shell reference drifted` / `Official Mac app.asar reference drifted` |
| Staged pack | `packStagedAppWithIntegrity` | `Staged package changed or ASAR drifted after snapshot` |
| Ad-hoc sign | `signAppBundleAdHoc` | `/usr/bin/codesign exited with <code>` after one retry |
| Deep verify | `/usr/bin/codesign --verify --deep --strict` | Non-zero `codesign` exit |
| Reconstructed invariant | `verifyReconstructedMacPackage` | Official payload copied, Mach-O mismatch, or unpacked runtime drift |
| Independent verify | `npm run verify` | Missing ASAR entry, native file, bundle id, or signature |

Success log from `package-macos.mjs`:

```text
Packaged application: <outputApp> (<nodeFileCount> native manifest entries, <runtimeFileCount> unpacked runtime files)
```

## Non-darwin abort

The reconstructed app is a macOS Apple Silicon Electron shell. `scripts/package-macos.mjs` throws immediately when `process.platform !== "darwin"` and never stages `dist/`. `scripts/package-fidelity-diagnostic.mjs` uses the same platform gate (`The fidelity diagnostic app can only be packaged on macOS`).

<Note>
Linux or Windows hosts can still run `npm run check`, `npm test`, and `npm run publication:check`. They cannot emit `dist/Grok Bot 0.18 Reconstructed.app`.
</Note>

Required host tools (hard-coded absolute paths in `SYSTEM_TOOLS`):

| Tool | Path | Packaging use |
| --- | --- | --- |
| `ditto` | `/usr/bin/ditto` | Copy cached `Grok Bot.app` into `dist/` |
| `xattr` | `/usr/bin/xattr` | `xattr -cr <outputApp>` |
| `plutil` | `/usr/bin/plutil` | Bundle id, display name, URL types, drop `ElectronAsarIntegrity` |
| `codesign` | `/usr/bin/codesign` | Ad-hoc sign and `--verify --deep --strict` |

Missing Xcode Command Line Tools surface as spawn/exit errors from those binaries, not as a custom packaging message.

## Missing renderer patch anchors

Default packaging keeps the checksum-pinned shipped renderer (`checksum-pinned-artifact-runtime`) and installs Settings Router by rewriting two original chunks under `dist/renderer/assets/*.js`.

Anchors that must appear **exactly once** in the scanned `.js` files:

| Label | Role |
| --- | --- |
| `settings registry` | Insert `{id:"router",label:"Router",icon:"git-branch"}` into the Settings nav array |
| `component insertion` | Prefix `function Sa(s){` with `RRouterPanel` / `RBoxRuntime` / `RRouterUsage` |
| `Router panel switch` | Route `x==="router"` to `RRouterPanel` |
| `Usage panel switch` | Replace Usage panel body with `RRouterUsage` |

Scan rule: exactly one registry candidate (`REGISTRY_BEFORE`) and exactly one panel candidate (`COMPONENT_ANCHOR` **and** `GENERAL_BEFORE` **and** `USAGE_BEFORE`). Any other cardinality throws:

```text
Expected one original Settings registry and panel chunk, found <registryCount>/<panelCount>.
```

A found string that is missing or duplicated throws:

```text
Original renderer <label> anchor is missing or ambiguous.
```

<AccordionGroup>
<Accordion title="Usual causes">

- `npm run bootstrap` did not hydrate `src/app/dist/renderer` from the pinned 0.18 `app.asar`.
- `GROK_BOT_RENDERER_SOURCE` replaced `dist/renderer` with a Vite (or other) tree that lacks the minified 0.18 Settings strings. Fidelity overlay does **not** restore `dist/renderer` when the renderer mode is `checksum-pinned-artifact-runtime`, so the patch still runs against the override.
- The shipped chunks were edited, minified differently, or already patched so the `before` strings no longer match once.

</Accordion>
<Accordion title="What packaging does not do">

`frontend/` is a design workspace. `npm run frontend:build` does not become the packaged UI unless you override the renderer, which then breaks these anchors. The patch writes `dist/renderer-router-extension.json` (`schemaVersion` 1, `mode` `original-renderer-settings-extension`) with per-chunk original/patched SHA-256 records.

</Accordion>
</AccordionGroup>

## Quarantine and xattr

After `ditto` copies the official runtime app to the output bundle, packaging runs:

```sh
/usr/bin/xattr -cr "dist/Grok Bot 0.18 Reconstructed.app"
```

The source DMG's quarantine/provenance belongs to Anysphere's signed artifact. Leaving those attributes on the reconstructed bundle makes Gatekeeper reject a valid **ad-hoc** signature before launch. `xattr` failure is a hard abort (`/usr/bin/xattr exited with <code>`). Do not skip this step to “keep” Gatekeeper metadata.

## Ad-hoc codesign retry

Signing is local-only. Identity is `AD_HOC_CODESIGN_IDENTITY` (`"-"`). Arguments:

```text
--force --deep --timestamp=none --sign - <bundle>
```

`signAppBundleAdHoc` invokes `/usr/bin/codesign` with non-interactive stdio (`ignore` / `inherit` / `inherit`). Empty target paths throw `An explicit application bundle path is required for ad-hoc signing.`

Before sign, packaging deletes `Contents/_CodeSignature`, then:

```mermaid
stateDiagram-v2
  [*] --> Unsigned: remove _CodeSignature
  Unsigned --> Signed: signAppBundleAdHoc pass 1
  Unsigned --> RetryWarned: pass 1 fails
  RetryWarned --> Signed: signAppBundleAdHoc pass 2
  RetryWarned --> Aborted: pass 2 fails
  Signed --> Verified: codesign --verify --deep --strict
  Verified --> [*]: verifyReconstructedMacPackage
  Aborted --> [*]
```

First-pass failure prints:

```text
Initial ad-hoc signing pass failed; retrying once: <error>
```

That retry exists because macOS can transiently deny replacing a nested framework signature while the copied runtime was still in use. A second pass is expected to succeed after the kernel releases the code object. There is **no** third attempt. A failed retry is the original `codesign` exit:

```text
/usr/bin/codesign exited with <code>
```

<Warning>
Do not weaken this gate (Developer ID, `--no-strict`, skipping `--deep`, or swallowing the retry). CONTRIBUTING requires checksum, bundle identity, and code-signing checks to stay fail-closed.
</Warning>

## Official reference vs reconstructed payload

`verifyOfficialMacReference` hashes the cached official app **before** the reconstructed ASAR is swapped in:

| Artifact | Path in official app | Expected SHA-256 |
| --- | --- | --- |
| Mach-O shell | `Contents/MacOS/Grok Bot` | `7fcdbad6228f0b7b90078c9f81968d9246c7f904e62cc5f2bce96dc504e3383e` |
| Official `app.asar` | `Contents/Resources/app.asar` | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |

`verifyReconstructedMacPackage` then requires:

- signature-excluded Mach-O structural match (`expectedSignatureExcludedMachOHash` `0076e3c5a6fda431b0a0c5bf25510c4b37562ff178d4f2053bac32f3885d6875`)
- reconstructed shell hash **must not** equal the official signed shell hash
- reconstructed `app.asar` hash **must not** equal the official ASAR hash
- unpacked trees `dist/deps`, `dist/native`, `dist/node-deps` byte-identical to the staged unpack
- `dist/deps/runtime-deps-manifest.json` with `platform` `darwin`, `arch` `arm64`, non-empty `nodeFiles`, and `resolutionClosure.mode` `byte-exact-sibling-package-copy`

Typical messages: `Official Mac shell reference drifted`, `Reconstructed package must not copy the official signed shell`, `Reconstructed package must not copy the official app.asar`, `Missing unpacked runtime root dist/deps`, `Packaged unpacked runtime drift at <path>`.

Plist mutations that must succeed (otherwise `plutil` exits non-zero):

- remove `ElectronAsarIntegrity`
- `CFBundleIdentifier` → `com.anysphere.sand.reconstructed`
- `CFBundleDisplayName` → `Grok Bot 0.18 Reconstructed`
- replace `CFBundleURLTypes` with a single `sand` URL scheme

`CFBundleName` / `CFBundleExecutable` stay `Grok Bot` so Electron helper names still match the reused 0.18 ABI.

## Independent verify: required ASAR entries

`npm run verify` (`scripts/verify.mjs`) inspects the selected `.app`, not `.build/app.asar`.

<ParamField body="--app" type="string">
Absolute path to a `.app` bundle. Default is `outputApp` (`dist/Grok Bot 0.18 Reconstructed.app`, or `GROK_BOT_OUTPUT_APP_NAME`). Usage: `node scripts/verify.mjs [--app /absolute/path/to/App.app]`.
</ParamField>

`resolvePackagedAppArtifacts` rejects non-`.app` paths (`Expected a .app bundle path`). ASAR listing is `@electron/asar` `listPackage` (entries with a leading `/`).

Required ASAR files:

```text
/dist/electron-main/main.cjs
/dist/electron-dev-controls/main.cjs
/dist/electron-preload/preload.cjs
/dist/electron-preload/preload-dev-controls.cjs
/dist/electron-preload/preload-webview.cjs
/dist/electron-preload/preload-vnc.cjs
/dist/node-agent-coordinator/main.cjs
/dist/host/host-main.cjs
/dist/host/agent-isolation/agent-store-worker.cjs
/dist/host/agent-isolation/transcript-mirror-worker.cjs
/dist/host/extensions/box-store-sync/box-store-vacuum-worker.cjs
/dist/host/extensions/content-search/search-index-worker.cjs
/dist/local-exec-daemon/main.cjs
/dist/renderer/index.html
/dist/reconstruction-build.json
/dist/runtime-composition-audit.json
/package.json
```

Missing any of those throws `ASAR is missing <path>`.

Also required:

| Check | Failure |
| --- | --- |
| ≥ 1,000 `// src/` markers in `src/app/dist` electron-main + host-main | `Expected at least 1,000 surviving evidence source markers, found N` |
| Unpacked `dist/deps/better-sqlite3/build/Release/better_sqlite3.node` | `access` fail |
| Unpacked `dist/native/sand-webauthn-signer` | `access` fail |
| Icon `dist/renderer/assets/app-icon-C7NKj2u7.png` vs `frontend/manifests/renderer-runtime-assets.json` | `ASAR is missing …` or `Packaged app icon differs from its renderer runtime manifest` |
| Any `dist/renderer/**/*.map` | `Packaged renderer contains source maps` |
| Node ABI entries in **both** ASAR and unpack (`tree_sitter_runtime_binding.node`, `tree_sitter_bash_binding.node`, `node-addon-api` / `node-gyp-build` `package.json`) | `ASAR is missing generated Node runtime entry <path>` |
| `CFBundleIdentifier` | `Unexpected reconstructed bundle ID` (must be `com.anysphere.sand.reconstructed`) |
| `CFBundleDisplayName` | `Unexpected reconstructed display name` |
| `ElectronAsarIntegrity` still in plist | `Stale ElectronAsarIntegrity metadata remains in the reconstructed application` |
| URL types | `Reconstructed application has no sand URL registration` |
| `codesign --verify --deep --strict` | `codesign` non-zero exit |

For `renderer.mode === "checksum-pinned-artifact-runtime"` (the `npm run package` path), provenance must bind `upstreamAppAsarSha256` to `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` and every `dist/renderer/` file must match that inventory. Clean-source renderer extra checks apply only if composition is `clean-source` (not the default package).

<RequestExample>
```sh title="Re-verify the default dist bundle"
npm run verify
```
</RequestExample>

<ResponseExample>
```text title="verify success (shape)"
Verified packaged ASAR <app>/Contents/Resources/app.asar.
Verified <N> executable clean-source runtimes, deterministic ASAR hashes, native dependencies, bundle identity, and code signature.
Documented non-clean runtime boundaries: <runtime names>. Evidence markers checked: <count>. Repository: <repoRoot>
```
</ResponseExample>

## Recover a failed package

<Steps>
<Step title="Confirm darwin and tools">
Run `uname -s` (`Darwin`) and that `/usr/bin/codesign`, `/usr/bin/xattr`, `/usr/bin/ditto`, and `/usr/bin/plutil` exist. Then `npm run check`.
</Step>
<Step title="If the abort is bootstrap-shaped">
DMG / `app.asar` checksum and hydration failures belong to bootstrap, not this page. Re-run `npm run bootstrap` so `src/app/dist` and the cached `Grok Bot.app` match the pinned 0.18 identities.
</Step>
<Step title="If the abort is a renderer anchor">
Unset `GROK_BOT_RENDERER_SOURCE`. Confirm `src/app/dist/renderer/assets` still contains the original Settings chunks (search for `id:"general",label:"General"`). Do not hand-edit those minified files.
</Step>
<Step title="If the abort is codesign">
Quit any running `Grok Bot` / reconstructed app that still holds the copied framework, re-run `npm run package`, and look for the one-shot retry warning. A second failure is not a quarantine issue if `xattr -cr` already ran; inspect `codesign` stderr.
</Step>
<Step title="Verify the bundle, then smoke">
`npm run verify` then `npm run smoke`. Do not ship a bundle that fails `--deep --strict` or is missing a required ASAR path.
</Step>
</Steps>

<Tip>
`GROK_BOT_OUTPUT_APP_NAME` only changes the `dist/` basename. Verify with `--app` if you set it, or `verify` will look at the default `Grok Bot 0.18 Reconstructed.app`.
</Tip>

## Next

<CardGroup>
<Card title="Package the macOS app" href="/package-macos-app">
Happy-path `npm run package` compile, patch, identity, sign, and output bundle.
</Card>
<Card title="Bootstrap failures" href="/bootstrap-failures">
LFS pointer DMGs, checksum mismatch, and `src/app/dist` hydration errors that starve packaging.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
`npm run verify` vs `npm run smoke`, updater `SAND_DISABLE_*` defaults.
</Card>
<Card title="Contributing" href="/contributing">
Required `check` / `package` commands. Do not weaken checksum or signing gates.
</Card>
</CardGroup>

---

## 22. Router and sandbox failures

> Unknown provider, Codex private auth.json, Claude Code not installed, missing OPENROUTER_API_KEY, and unowned grok-bot-local-vm container.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/22-router-and-sandbox-failures.md
- Generated: 2026-08-24T05:57:42.918Z

### Source Files

- `source/host/extensions/inference/provider-session.ts`
- `source/shared/node/inference-router-local.ts`
- `source/electron-main/box/local-docker-host-connector.ts`
- `source/electron-main/main-edge.ts`
- `tests/router-settings.test.mjs`
- `tests/codex-direct-responses.test.mjs`

---
title: "Router and sandbox failures"
description: "Unknown provider, Codex private auth.json, Claude Code not installed, missing OPENROUTER_API_KEY, and unowned grok-bot-local-vm container."
---

Settings → Router writes `inferenceProvider` and `boxRuntime` through `window.desktop.agent.setInferenceRouter` / `setBoxRuntime`. Invalid ids fail the RPC immediately. Codex, Claude Code, and OpenRouter credentials are not required to *select* a provider; they fail later, when a routed `sendPrompt` runs. Local Docker is the opposite: toggling **Use local Docker VM** starts or stops `grok-bot-local-vm` before the setting sticks, and a start/stop error rolls `boxRuntime` back.

<Warning>
Selecting `codex`, `claude-code`, or `openrouter` can succeed while the next agent turn still fails. The Settings panel shows local CLI status and a red error string; a routed turn also appends `Router error: …` to the local transcript.
</Warning>

## Failure classes

| Surface | When it runs | On failure |
|---|---|---|
| `setInferenceRouter({ provider })` | Settings provider change | Throws `Unknown inference provider.` Did not persist. |
| `getInferenceRouter()` | Settings load | Unknown stored id is returned as `cursor`. Box-settings read failure is ignored. |
| `setBoxRuntime({ mode })` | **Use local Docker VM** switch | Throws after reverting `boxRuntime`. Coordinator is *not* restarted. |
| `getBoxRuntime()` | Settings load | Throws `Unknown box runtime.` if the store is not `remote` or `local-docker`. |
| Routed `sendPrompt` | Agent turn when provider ≠ `cursor` | Transcript line `Router error: <message>`. RPC still returns `{ accepted: true }`. |

Allowed `SandInferenceProvider` ids: `cursor`, `claude-code`, `codex`, `openrouter` (default `cursor`). Allowed `SandBoxRuntime` values: `remote`, `local-docker` (default `remote`).

`settings.json` version 1 silently drops unknown `inferenceProvider` / `boxRuntime` on load, then getters default to `cursor` and `remote`. The RPC invariants fire only on the live getter/setter path.

```mermaid
stateDiagram-v2
  [*] --> remote: default
  remote --> starting: setBoxRuntime local-docker
  starting --> local_docker: start grok-bot-local-vm + health + restartCoordinator
  starting --> remote: start throws, persist remote
  local_docker --> stopping: setBoxRuntime remote
  stopping --> remote: stop owned container + restartCoordinator
  stopping --> local_docker: stop throws, persist local-docker
```

## Diagnose from Settings

The patched Settings Router panel calls `getInferenceRouter` and `getBoxRuntime` on load. Provider status for local CLIs comes from `local` on the inference payload, not from a second probe.

| Provider | Ready in Settings | Not ready copy |
|---|---|---|
| `cursor` | Signed-in Cursor account (no extra key) | Account copy only |
| `claude-code` | CLI path found **and** (`~/.claude/.credentials.json` exists **or** `ANTHROPIC_API_KEY` is set) | `Not installed` or `Sign in with claude` |
| `codex` | `$CODEX_HOME/auth.json` or `~/.codex/auth.json` exists **and** is a usable ChatGPT login | `Not installed` or `Sign in with codex login` |
| `openrouter` | Secret key `OPENROUTER_API_KEY` listed by `window.desktop.secrets.list()` | Paste field: `Paste API key` / `Replace saved key` |

Codex inference does **not** invoke the `codex` binary. `installed` for Codex means the auth file exists. Claude Code **does** require the `claude` executable on the turn path even if `ANTHROPIC_API_KEY` makes Settings show Ready.

<ParamField body="provider" type="SandInferenceProvider" required>
RPC body for `setInferenceRouter`. Must be one of `cursor`, `claude-code`, `codex`, `openrouter`.
</ParamField>

<ParamField body="mode" type="SandBoxRuntime" required>
RPC body for `setBoxRuntime`. Must be `remote` or `local-docker`.
</ParamField>

<ResponseField name="local.codex.authenticated" type="boolean">
True only when `auth.json` is a non-symlink regular file, group/other bits are zero (`mode & 0o077 === 0`), `auth_mode` is `chatgpt`, and `tokens.access_token`, `refresh_token`, `id_token`, and `account_id` are non-empty strings.
</ResponseField>

<ResponseField name="status.detail" type="string">
Human Docker status from `getLocalDockerStatus`, including unowned-container copy.
</ResponseField>

## Unknown provider

`window.desktop.agent.setInferenceRouter(provider)` maps to main-edge `setInferenceRouter`. A value that fails `isSandInferenceProvider` throws:

```text
Unknown inference provider.
```

The Settings dropdown only emits the four known ids, so this is an RPC/client bug or a hand-crafted call. Persist happens only after the invariant.

<Tabs>
<Tab title="RPC">
```javascript
await window.desktop.agent.setInferenceRouter("not-a-provider")
// Error: Unknown inference provider.
```
</Tab>
<Tab title="settings.json load">
Unknown `inferenceProvider` is omitted on parse. `getInferenceProvider()` then returns `cursor`. `getInferenceRouter()` also coerces a live unknown store value to `cursor`.
</Tab>
<Tab title="frontend workspace">
`saveRouterProvider` in the Vite overlay throws `Unknown router provider.` Packaged Settings does **not** use `settings.router-provider.v1`; that key is design-workspace only.
</Tab>
</Tabs>

Unknown `mode` on `getBoxRuntime` / `setBoxRuntime` throws:

```text
Unknown box runtime.
```

## Codex private auth.json

Codex turns read `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`) and POST to `https://chatgpt.com/backend-api/codex/responses` with `Authorization: Bearer` and `ChatGPT-Account-Id`.

| Error | Cause | Repair |
|---|---|---|
| `Codex login credentials must be a private direct regular file.` | Missing file, directory, symlink, or mode allows group/other (`chmod` not `0600`) | `chmod 600 ~/.codex/auth.json`. Do not point `CODEX_HOME` at a symlink of `auth.json`. |
| `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.` | `auth_mode` ≠ `chatgpt` or any of the four tokens missing | `codex login`, then reopen the app |
| `Codex login expired and its refresh identity is invalid. Run \`codex login\` again.` | `id_token` has no usable `aud` as OAuth `client_id` | `codex login` |
| `Codex login expired and could not be refreshed. Run \`codex login\` again.` | `https://auth.openai.com/oauth/token` non-OK | `codex login` |
| `Codex returned an invalid refreshed login. Run \`codex login\` again.` | Refresh JSON lacks `access_token` | `codex login` |

Refresh writes `auth.json` via a `0o600` temp file then `rename`. A 401 on the Responses call triggers one refresh-and-retry.

Default model is `SAND_CODEX_MODEL`, else `model` in `$CODEX_HOME/config.toml`, else `gpt-5.4`.

<AccordionGroup>
<Accordion title="Codex transport errors after auth succeeds">

| Error | Meaning |
|---|---|
| `Codex direct request failed (<status>[: <body>]).` | Non-OK HTTP; body truncated to 4096 chars |
| `Codex direct response did not include a stream.` | Empty body |
| `Codex direct response contained malformed SSE JSON.` | SSE `data:` is not JSON |
| `Codex direct response ended with an incomplete SSE event.` | Truncated stream |
| `Codex direct response ended without response.completed.` | Stream finished without completion |
| `Codex direct response failed: …` | `response.failed` or `error` event |
| `Codex requested a tool but Grok Bot did not provide an executor.` | Tool call with no `executeTool` |
| `Codex exceeded Grok Bot's 8-step tool limit.` | Default `maxSteps` 8 when tools are present |

Unknown tool names are not fatal: the transport returns `{ isError: true, error: "Unknown Grok Bot tool: <name>" }` as `function_call_output` and continues.
</Accordion>
</AccordionGroup>

## Claude Code not installed

A `claude-code` turn resolves the CLI with `resolveClaudeCodeCliPath()`:

1. `CLAUDE_CODE_PATH`
2. `~/.local/bin/claude`
3. `~/.claude/local/claude`
4. `PATH` entries named `claude`
5. `/opt/homebrew/bin/claude`
6. `/usr/local/bin/claude`

If none exist:

```text
Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.
```

Install Claude Code, sign in, then reopen Grok Bot so the path probe runs again.

| Error | Meaning |
|---|---|
| `Claude Code ended without a result.` | Agent SDK stream had no `result` message |
| `Claude Code failed (<subtype>).` | Non-success result with empty `errors` |
| joined `final.errors` | SDK reported those strings |

The query uses `pathToClaudeCodeExecutable`, `cwd` = Sand root, `permissionMode: "default"`, `persistSession: false`, `maxTurns` 1 without MCP or 8 with the `grok_bot_plugins` HTTP MCP bridge. Optional `SAND_CLAUDE_MODEL` sets the model.

<Note>
Settings may show Ready when `ANTHROPIC_API_KEY` is set even if `claude` is missing. The turn still throws the not-installed error until the executable resolves.
</Note>

## Missing OPENROUTER_API_KEY

OpenRouter reads, in order:

1. `process.env.OPENROUTER_API_KEY` (trimmed)
2. `secrets.OPENROUTER_API_KEY` in `box-secrets.json` (Sand root)

Empty after both:

```text
OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.
```

<Steps>
<Step title="Save the key in Settings">
On **OpenRouter**, paste into the API key field and click **Save**. The panel calls `window.desktop.secrets.upsert({ OPENROUTER_API_KEY: "<key>" })` over `sand:secrets-upsert`.
</Step>
<Step title="Or inject the environment">
Launch with `OPENROUTER_API_KEY` set. Env wins over the secrets file.
</Step>
<Step title="Retry the turn">
The key is read at request time, not at `setInferenceRouter`. Re-send after save; you do not need to re-select OpenRouter.
</Step>
</Steps>

Default model is `SAND_OPENROUTER_MODEL` or `openai/gpt-5.2`. Base URL is `https://openrouter.ai/api/v1`.

## Unowned grok-bot-local-vm

Local Docker owns a container only when label `com.grok-bot.local-vm` is `"1"` (`LOCAL_DOCKER_OWNER_LABEL` = `com.grok-bot.local-vm=1`). Name is always `grok-bot-local-vm`. Image is `public.ecr.aws/k0i0n2g5/cursorenvironments/universal:sand-box-latest`. Gateway is `http://127.0.0.1:1340/health` with `Authorization: Bearer <token>` from `local-docker-vm.json` next to `settings.json`. Schema version `6`.

| Status / error | Condition |
|---|---|
| `Docker is not installed.` / `Docker is not running.` | `docker info` failed (`available: false`) |
| `Ready to create the local VM.` | Daemon up, container missing |
| `Container grok-bot-local-vm exists but is not owned by Grok Bot.` | Name taken, owner label missing (`ready: false`) |
| `Local Docker VM cannot use grok-bot-local-vm: an unowned container already has that name.` | `startLocalDockerBox` / connect |
| `Refusing to stop unowned container grok-bot-local-vm.` | Toggle back to `remote` while the name is unowned |
| `Local Docker VM is selected, but Docker is unavailable: …` | Local mode selected, daemon down |
| `Local Docker VM container uses unexpected image <image>. Remove it explicitly before changing images.` | Owned container, wrong image |
| `Could not replace the local VM with the current app runtime: …` | `docker rm --force` failed on schema/host-sha mismatch |
| `The reconstructed runtime is unavailable at …; refusing to start a stock local VM.` | Packaged `host-main.cjs` / `box-exec-daemon/main.cjs` missing |
| `Content-addressed local runtime <path> has unexpected bytes.` | Staged host file hash collision |
| `Could not start the local Docker VM: …` / `Could not create the local Docker VM: …` | `docker start` / `docker run` failed |
| `Local Docker VM stopped before its gateway became ready.` plus last 80 log lines | Container exited during the 180s wait |
| `Local Docker VM did not expose its gateway within three minutes.` | Health never OK |
| `Docker returned malformed container inspection data.` | `docker inspect` JSON unusable |
| `Could not restart the local Docker VM: …` | Recreate path `docker restart` failed |

On `setBoxRuntime("local-docker")` the edge persists `local-docker`, runs `startLocalDockerBox`, and on throw writes `remote` then rethrows. On `setBoxRuntime("remote")` it persists `remote`, runs `stopLocalDockerBox`, and on throw writes `local-docker`. Success restarts the coordinator. The Settings switch restores the previous visual mode and shows the error in red.

<Steps>
<Step title="Confirm ownership">
```bash
docker inspect grok-bot-local-vm --format '{{json .Config.Labels}}'
```
Grok Bot requires `"com.grok-bot.local-vm":"1"`. Also expect `com.grok-bot.local-vm.schema-version=6` and a `com.grok-bot.local-vm.host-sha256` matching the staged `host-main.cjs`.
</Step>
<Step title="If the name is a foreign container">
Stop using that name from Grok Bot until you rename or remove the foreign container yourself. The app will not `stop` or `rm` an unowned `grok-bot-local-vm`.
</Step>
<Step title="If Docker is down">
Start Docker Desktop, then toggle **Use local Docker VM** again. Health must succeed on loopback `127.0.0.1:1340` only.
</Step>
<Step title="If start still fails">
Expect automatic rollback to `remote`. Fix the thrown message, then toggle again. Do not assume the coordinator switched; restart happens only after a successful start/stop.
</Step>
</Steps>

<Warning>
An owned container with a mismatched schema, host SHA-256, or missing inference-credential label **is** force-removed and recreated. An unowned container with the same name is never removed.
</Warning>

Loopback publishes on create: `127.0.0.1:1337`, `1339`, `1340`, `6080`, `6081`, `8790`. Optional read-only mounts of `~/.codex` → `/root/.codex` and `~/.claude` → `/root/.claude` if those directories exist.

## Turn-time router errors

When `inferenceProvider` is not `cursor`, the coordinator handles `sendPrompt` locally. Missing `agentId` or `prompt` throws `Local inference routing requires an agentId and prompt` (same `Router error:` transcript prefix). Claude Code uses the MCP HTTP bridge; Codex and OpenRouter execute `listRoutedMcpTools` / `executeRoutedMcpTool` directly.

<RequestExample>
```json
{
  "method": "sendPrompt",
  "args": {
    "agentId": "agent-1",
    "prompt": "Summarize inbox",
    "clientNonce": "optional"
  }
}
```
</RequestExample>

<ResponseExample>
```json
{
  "accepted": true,
  "clientNonce": "optional",
  "provider": "openrouter"
}
```
</ResponseExample>

Acceptance is queued; credential failures still accept the RPC and write an assistant row `Router error: OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.`

## Related pages

<CardGroup>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Persist `inferenceProvider` and satisfy Cursor, Claude Code, Codex, or OpenRouter.
</Card>
<Card title="Enable the local Docker sandbox" href="/enable-local-docker">
Toggle local Docker, wait for `http://127.0.0.1:1340/health`, roll back on start failure.
</Card>
<Card title="Inference router" href="/inference-router">
Provider ids, transcript `schemaVersion` 2, usage `schemaVersion` 1.
</Card>
<Card title="Box runtime" href="/box-runtime">
`remote` vs `local-docker`, coordinator restart, loopback gateway ownership.
</Card>
<Card title="Local Docker connector" href="/docker-sandbox-reference">
Image, owner label, schema 6, health probe, content-addressed mounts.
</Card>
<Card title="Environment variables" href="/environment-variables">
`OPENROUTER_API_KEY`, `CODEX_HOME`, `CLAUDE_CODE_PATH`, `SAND_*` model overrides.
</Card>
</CardGroup>

---

## 23. Verify and smoke

> npm run verify required ASAR paths and evidence markers; npm run smoke native e2e with 12s timeout; updater guard SAND_DISABLE_* defaults.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/23-verify-and-smoke.md
- Generated: 2026-08-24T05:58:43.918Z

### Source Files

- `scripts/verify.mjs`
- `scripts/smoke.mjs`
- `scripts/native-e2e-check.mjs`
- `scripts/lib/build-asar.mjs`
- `tests/reconstructed-updater-guard.test.mjs`
- `package.json`

---
title: "Verify and smoke"
description: "npm run verify required ASAR paths and evidence markers; npm run smoke native e2e with 12s timeout; updater guard SAND_DISABLE_* defaults."
---

`npm run verify` and `npm run smoke` are post-package gates on the reconstructed macOS app. `verify` (`scripts/verify.mjs`) fail-closes on ASAR membership, unpacked natives, composition provenance, reconstructed identity, and `codesign --verify --deep --strict`. `smoke` (`scripts/smoke.mjs`) calls `runNativeE2E` with `timeoutMs: 12_000` against the same default bundle and never launches unless the structural graph and `/Applications` placement pass. The Electron-main artifact fallback prepends an idempotent updater guard that sets `SAND_DISABLE_UPDATES`, `SAND_DISABLE_SENTRY`, and `SAND_DISABLE_TELEMETRY` with `??= "1"`.

`npm run package` signs the bundle and runs `verifyReconstructedMacPackage`; it does not invoke `scripts/verify.mjs`. After packaging, run verify (and smoke if you need a native observation) yourself.

## Commands

| Script | Implementation | Default target | Timeout |
| --- | --- | --- | --- |
| `npm run verify` | `node scripts/verify.mjs` | `outputApp` → `dist/Grok Bot 0.18 Reconstructed.app` | none (throws on first failure) |
| `npm run smoke` | `node scripts/smoke.mjs` | same `outputApp`; `structuralOnly: false` | **12_000 ms** |
| (direct) | `node scripts/native-e2e-check.mjs` | `dist/Grok Bot 0.18 Reconstructed.app`; default `timeoutMs: 15_000` | CLI `--timeout-ms` |

Override the verify bundle with an **absolute** path:

```sh
node scripts/verify.mjs --app /absolute/path/to/Grok\ Bot\ 0.18\ Reconstructed.app
```

`GROK_BOT_OUTPUT_APP_NAME` changes only the basename under `dist/`; it cannot retarget `/Applications`. For a production-startup observation against an installed copy, call native e2e directly:

```sh
node scripts/native-e2e-check.mjs --app /Applications/Grok\ Bot\ 0.18\ Reconstructed.app --timeout-ms 12000
```

<Note>
`npm run smoke` always points at `outputApp` under `dist/`. Production launch is refused unless `realpath(app)` is inside `/Applications`, so the default smoke command is a structural-plus-prerequisite run, not an in-place launch of the dist payload.
</Note>

## Verify

`scripts/verify.mjs` resolves `Contents/Resources/app.asar` and `app.asar.unpacked` via `resolvePackagedAppArtifacts`. Any missing path or mismatched hash throws. Success prints the ASAR path, clean-source runtime count, documented non-clean boundaries, evidence-marker count, and repo root.

### Evidence markers

Before ASAR checks, verify reads hydrated `src/app/dist/electron-main/main.cjs` and `src/app/dist/host/host-main.cjs` and counts lines matching `/^\/\/ src\//`. Fewer than **1,000** combined markers throws `Expected at least 1,000 surviving evidence source markers`.

### Required ASAR membership

These archive paths must appear in `listPackage(app.asar)` (leading `/` as stored by `@electron/asar`):

| Path | Role |
| --- | --- |
| `/package.json` | packaged app manifest |
| `/dist/electron-main/main.cjs` | Electron main |
| `/dist/electron-dev-controls/main.cjs` | dev-controls main |
| `/dist/electron-preload/preload.cjs` | primary preload |
| `/dist/electron-preload/preload-dev-controls.cjs` | dev-controls preload |
| `/dist/electron-preload/preload-webview.cjs` | webview preload |
| `/dist/electron-preload/preload-vnc.cjs` | VNC preload |
| `/dist/node-agent-coordinator/main.cjs` | coordinator |
| `/dist/host/host-main.cjs` | host |
| `/dist/host/agent-isolation/agent-store-worker.cjs` | agent-store worker |
| `/dist/host/agent-isolation/transcript-mirror-worker.cjs` | transcript-mirror worker |
| `/dist/host/extensions/box-store-sync/box-store-vacuum-worker.cjs` | box-store vacuum worker |
| `/dist/host/extensions/content-search/search-index-worker.cjs` | search-index worker |
| `/dist/local-exec-daemon/main.cjs` | local-exec daemon |
| `/dist/renderer/index.html` | renderer index |
| `/dist/reconstruction-build.json` | build manifest |
| `/dist/runtime-composition-audit.json` | composition audit |

Unpacked natives that must exist on disk (not only in the ASAR listing):

- `app.asar.unpacked/dist/deps/better-sqlite3/build/Release/better_sqlite3.node`
- `app.asar.unpacked/dist/native/sand-webauthn-signer`
- `dist/node-deps/tree-sitter/build/Release/tree_sitter_runtime_binding.node`
- `dist/node-deps/tree-sitter-bash/build/Release/tree_sitter_bash_binding.node`
- `dist/node-deps/node_modules/node-addon-api/package.json`
- `dist/node-deps/node_modules/node-gyp-build/package.json`

Additional renderer constraints:

- `frontend/manifests/renderer-runtime-assets.json` must record `app-icon-C7NKj2u7.png` with a SHA-256; the packaged file must match bytes and hash.
- No `dist/renderer/**/*.map` source maps.
- `dist/renderer/index.html` must use file-relative `src="./assets/..."`.

### Composition and provenance

`dist/reconstruction-build.json` `runtimeComposition` must match `dist/runtime-composition-audit.json`. Each `buildManifest.outputs[]` entry is re-hashed from the ASAR.

| Runtime mode | Verify rule |
| --- | --- |
| `clean-source` | Must **not** ship `dist/recovered-source/...` for that runtime. Bundles must contain `// Deterministic clean-source`. Host/Electron-main require `dist/host-production-bindings.json` / `dist/electron-main-production-bindings.json` with `status: "validated-clean-source"` and no `src/app`, `dist/deps`, or `recovered/source-capsules` binding modules. Clean renderer must prove `frontend/src/main.tsx`, **11** reviewed route contracts, **5** composed feature surfaces, **0** findings, and must not activate `index-UbX-y3il.js`. |
| `artifact-fallback` | Must ship the recovered-source path (`dist/recovered-source/electron-main/main.cjs` or `.../host/host-main.cjs`) and a `sourceBundle`. Packaged `dist/electron-main/main.cjs` must equal `prepareReconstructedElectronMainArtifactFallback` of the immutable evidence bytes. |
| `checksum-pinned-artifact-runtime` (renderer) | Provenance `schemaVersion` 1, `upstreamAppAsarSha256` equals `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58`, every inventoried file matches bytes/SHA-256, no undeclared renderer files. Default `npm run package` uses this renderer mode. |

Any other renderer mode throws `Unsupported packaged renderer mode`.

### Bundle identity and signature

| Check | Required value |
| --- | --- |
| `CFBundleIdentifier` | `com.anysphere.sand.reconstructed` |
| `CFBundleDisplayName` | `Grok Bot 0.18 Reconstructed` |
| `ElectronAsarIntegrity` | must be absent |
| URL schemes | `sand` present under `CFBundleURLTypes` |
| codesign | `codesign --verify --deep --strict` on the `.app` |

## Smoke and native e2e

`scripts/smoke.mjs` prints each diagnostic as `STATUS check: detail`, then `Smoke verification: PASS|FAIL|PREREQUISITE`.

```js
const report = await runNativeE2E({
  appPath: outputApp,
  payloadPath: outputApp,
  structuralOnly: false,
  timeoutMs: 12_000,
});
process.exitCode = report.status === "pass" ? 0 : report.status === "prerequisite" ? 2 : 1;
```

Direct `native-e2e-check.mjs` flags: `--app`, `--payload`, `--structural-only`, `--json`, `--timeout-ms`. Unknown flags throw.

```mermaid
stateDiagram-v2
  [*] --> OpenPayload: runNativeE2E
  OpenPayload --> Prerequisite: missing app or unreadable ASAR
  OpenPayload --> Structural: inspectPackagedArtifacts + verifyEntrypointGraph
  Structural --> SkipLaunch: .app requested and a structural check failed
  SkipLaunch --> Prerequisite: runtime:launch skipped
  Structural --> Launch: .app, structural pass
  Launch --> Prerequisite: not Darwin, not under /Applications, unsigned, or no use-mock-keychain
  Launch --> Observe: isolated profile, 12s window, --use-mock-keychain
  Observe --> Pass: renderer present, host/coordinator absent, no fatal or opaque-fallback logs
  Observe --> Fail: early exit, missing renderer, unexpected host/coordinator, fatal or opaque-fallback logs
```

### Structural artifacts (smoke)

Smoke’s `REQUIRED_PACKAGED_ARTIFACTS` is a **subset** of verify, plus a clean-renderer provenance file that the default fidelity package does **not** ship:

- `package.json` with `main === dist/electron-main/main.cjs` and `sandLab` absent or exactly `false`
- `dist/electron-main/main.cjs`, preloads, coordinator, host, local-exec daemon, `dist/renderer/index.html`
- `dist/renderer/renderer-source-provenance.json` (clean-source renderer only; fidelity packages use `dist/renderer-artifact-provenance.json` instead)

A failed structural check skips launch and returns status `prerequisite` (exit **2**), not a runtime fail.

### Entrypoint graph

`verifyEntrypointGraph` treats each `PRODUCTION_ENTRYPOINTS` role as **clean-source**. It fails as `opaque bundle fallback` when the artifact:

- lacks provenance for the expected `source/` or `frontend/src/` entry
- is byte-identical to `src/app/...`
- declares a non-`clean-source` composition mode
- imports `src/app`
- (renderer) does not prove **11/11** `reviewed` + `cleanComposition: "present"` route contracts

Default `npm run package` composition is `fidelityRuntimeComposition`: renderer `checksum-pinned-artifact-runtime`, Electron-main and host `artifact-fallback`. That package can pass `npm run verify` and still fail smoke’s entrypoint graph.

### Production launch constraints

`launchPackagedApp` only runs when the path ends in `.app` and structural checks passed.

<ParamField body="platform" type="string" required>
Must be `darwin`. Other platforms skip with `runtime:platform`.
</ParamField>

<ParamField body="applications location" type="path" required>
`realpath(app)` must be inside `/Applications`. Dist payloads fail `runtime:applications-location` with `native launch refused before the immutable move-to-Applications prompt`.
</ParamField>

<ParamField body="codesign" type="command" required>
`codesign --verify --deep --strict --verbose=2`. Unsigned bundles are a prerequisite, not a runtime fail.
</ParamField>

<ParamField body="mock keychain" type="bytes" required>
`Contents/Frameworks/Electron Framework.framework/Electron Framework` must contain the UTF-8 needle `use-mock-keychain`. Launch args are exactly `--use-mock-keychain`.
</ParamField>

### Isolated environment

`createNativeTestEnvironment` copies `process.env`, **deletes** the production denylist keys, then sets:

| Key | Value |
| --- | --- |
| `SAND_USER_DATA_DIR` | `mkdtemp(.../grok-bot-native-e2e-)` |
| `SAND_DATA_ROOT` | `<userData>/sand-data` |
| `SAND_DISABLE_UPDATES` | `"1"` (overwrite) |
| `SAND_DISABLE_TELEMETRY` | `"1"` |
| `SAND_DISABLE_ANALYTICS` | `"1"` |

Denylist keys (must be absent on the spawned env): `GROK_BOT_RECONSTRUCTED_DEV`, `ELECTRON_RUN_AS_NODE`, `VITE_DEV_SERVER_URL`, `SAND_DEV_LOGIN`, `SAND_DEV_LOGIN_EMAIL`, `SAND_BACKEND_URL`, `CURSOR_API_BASE_URL`, `NODE_PATH`, `NODE_OPTIONS`, `NODE_EXTRA_CA_CERTS`, `NODE_V8_COVERAGE`, `ELECTRON_LOG_FILE`, `ELECTRON_ENABLE_LOGGING`, `SAND_HOST_GATEWAY_URL`, `SAND_HOST_GATEWAY_TOKEN`, `SAND_HOST_GATEWAY_NETWORK_TOKEN`, `SAND_FEATURE_GATE_OVERRIDES`, `SAND_MODEL_EXPERIMENT_OVERRIDE`.

Poll interval is **250 ms**. After the window the process group gets `SIGTERM`, then `SIGKILL`, and the temp profile is deleted.

### Fresh-profile pass criteria

`classifyFreshProfileRuntimeEvidence` **requires** a renderer descendant and **requires host and coordinator to stay absent** for a fresh profile. `runtime:daemon-observation` always passes (presence is recorded, not gated).

Fatal log patterns include uncaught exception, unhandled rejection, fatal error, segmentation fault, renderer crash, failed-to-load entry, `cannot find module`, `syntaxerror:`. Opaque-fallback patterns include `opaque bundle fallback`, shipped/legacy bundle fallback, `src/app` loading, and `GROK_BOT_ALLOW_OPAQUE_FALLBACK`.

### Observation report

Reports are `schemaVersion: 1`, `reportKind: "native-observation"`, `producer: "native-e2e"`.

| `observationClass` | When |
| --- | --- |
| `deterministic-prerequisite-no-launch` | missing payload, failed structural graph, or launch prerequisites |
| `admissible-production-startup-observation` | launch ran |
| `structural-only` | `--structural-only` or non-`.app` payload |

`--json` prints the full report. Text mode prints `Native E2E verification: STATUS` (or smoke’s `Smoke verification:` line).

## Updater guard

`reconstructedUpdaterGuard` in `scripts/lib/build-asar.mjs` is prepended to Electron-main artifact fallbacks by `prepareReconstructedElectronMainArtifactFallback` (also enables reconstructed instance/dev seams when `GROK_BOT_BUILD_DEV_APP=1`). `applyReconstructedUpdaterGuard` is idempotent: a source that already starts with the guard is left unchanged.

```js
// Reconstructed-build guard: do not consume official update or telemetry services.
process.env.SAND_DISABLE_UPDATES ??= "1";
process.env.SAND_DISABLE_SENTRY ??= "1";
process.env.SAND_DISABLE_TELEMETRY ??= "1";
```

`tests/reconstructed-updater-guard.test.mjs` asserts those three `??= "1"` assignments, idempotence, and that fallback packaging still starts with the same guard.

<Warning>
The packaged guard uses nullish assignment (`??=`) so a caller can still force-enable a service. Smoke **overwrites** `SAND_DISABLE_UPDATES` and `SAND_DISABLE_TELEMETRY` to `"1"` and also sets `SAND_DISABLE_ANALYTICS` (not part of the packaged guard). The packaged guard sets `SAND_DISABLE_SENTRY`; smoke does not.
</Warning>

## Exit codes

| Code | `verify` | `smoke` / `native-e2e-check` |
| --- | --- | --- |
| `0` | all checks passed | `report.status === "pass"` |
| `1` | uncaught `Error` (Node default) | `fail` |
| `2` | not used | `prerequisite` (do not treat as a product crash) |

## Operator procedure

<Steps>
<Step title="Package on Darwin">
Run `npm run package` so `dist/Grok Bot 0.18 Reconstructed.app` exists, is ad-hoc signed, and uses reconstructed identity. Packaging already runs `codesign --verify --deep --strict` and `verifyReconstructedMacPackage`.
</Step>
<Step title="Verify the dist app">
Run `npm run verify`. Confirm the log names the ASAR, clean-source count, non-clean boundaries (`electron-main`, `host`, `renderer` on the default fidelity package), and evidence-marker count ≥ 1000.
</Step>
<Step title="Optional: install then observe startup">
Copy the signed app into `/Applications`. Run `node scripts/native-e2e-check.mjs --app /Applications/Grok Bot 0.18 Reconstructed.app --timeout-ms 12000`. Expect `prerequisite` unless the payload also satisfies smoke’s clean-source entrypoint graph (not the default checksum-pinned renderer).
</Step>
</Steps>

## Failures

<AccordionGroup>
<Accordion title="ASAR is missing /dist/...">
The packaged archive listing does not contain a required runtime, renderer index, or composition JSON. Re-run `npm run package`; do not hand-edit `app.asar`. See packaging failures for missing renderer patch anchors and unsigned replacements.
</Accordion>
<Accordion title="Expected at least 1,000 surviving evidence source markers">
`src/app/dist` is missing or not the hydrated 0.18 payload. Run bootstrap before package/verify.
</Accordion>
<Accordion title="Unexpected reconstructed bundle ID / display name">
`Info.plist` was not rewritten to `com.anysphere.sand.reconstructed` / `Grok Bot 0.18 Reconstructed`, or you pointed `--app` at the official reference app.
</Accordion>
<Accordion title="Stale ElectronAsarIntegrity metadata">
Packaging must `plutil -remove ElectronAsarIntegrity` after replacing `app.asar`. Remaining integrity metadata fails verify.
</Accordion>
<Accordion title="Smoke: Missing required packaged artifact dist/renderer/renderer-source-provenance.json">
Default fidelity packages ship `dist/renderer-artifact-provenance.json`. Smoke’s artifact list still requires the clean-source provenance filename. Exit 2 is expected for `npm run package` output.
</Accordion>
<Accordion title="Smoke: opaque bundle fallback">
Entrypoint graph rejected artifact-fallback main/host or checksum-pinned renderer. That is a stricter policy than `npm run verify`.
</Accordion>
<Accordion title="runtime:applications-location">
Launch is refused for anything whose realpath is not under `/Applications`, including `dist/*.app`.
</Accordion>
<Accordion title="runtime:keychain-isolation">
The bundled Electron Framework binary does not contain `use-mock-keychain`. Native launch is refused.
</Accordion>
<Accordion title="runtime:host or runtime:coordinator fail after launch">
Fresh-profile policy treats host or coordinator evidence as failure. Renderer must appear; host and coordinator must not.
</Accordion>
</AccordionGroup>

## Next

<CardGroup>
<Card title="Package the macOS app" href="/package-macos-app">
How `npm run package` compiles runtimes, replaces `app.asar`, sets reconstructed identity, and ad-hoc signs.
</Card>
<Card title="Packaging failures" href="/packaging-failures">
Non-darwin abort, missing renderer patch anchors, codesign retry, quarantine, and verify ASAR gaps.
</Card>
<Card title="npm scripts" href="/npm-scripts">
Exact `package.json` commands including `check`, `package`, `verify`, and `smoke`.
</Card>
<Card title="Environment variables" href="/environment-variables">
`GROK_BOT_OUTPUT_APP_NAME`, `SAND_DISABLE_*`, and related packaging overrides.
</Card>
<Card title="Reconstruction boundary" href="/reconstruction-boundary">
Why the default packaged renderer stays checksum-pinned while `source/` runtimes compile separately.
</Card>
<Card title="Contributing" href="/contributing">
Required `check` and `package`/`verify` gates; do not weaken checksum or signing checks.
</Card>
</CardGroup>

---

## 24. Preserve original installers

> Git LFS 0.18.0 macOS DMG and Windows setup.exe identities, artifacts.json schemaVersion 1, and shasum -c SHA256SUMS.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/24-preserve-original-installers.md
- Generated: 2026-08-24T05:58:39.547Z

### Source Files

- `research-archives/README.md`
- `research-archives/original/0.18.0/artifacts.json`
- `research-archives/original/0.18.0/SHA256SUMS`
- `tests/research-archives.test.mjs`
- `PROVENANCE.md`
- `scripts/lib/config.mjs`

---
title: "Preserve original installers"
description: "Git LFS 0.18.0 macOS DMG and Windows setup.exe identities, artifacts.json schemaVersion 1, and shasum -c SHA256SUMS."
---

`research-archives/original/0.18.0/` holds the public Grok Bot 0.18.0 installers as Git LFS objects, plus a committed inventory (`artifacts.json`, `schemaVersion` 1) and `SHA256SUMS`. Those files are preservation inputs, not reconstructed build outputs. `npm run bootstrap` consumes only the macOS arm64 DMG after its SHA-256 matches `dmgSha256` in `scripts/lib/config.mjs`; the Windows x64 setup.exe is archived for identity and research continuity and is not a packaging input.

<Warning>
A checked-out LFS pointer is a ~134-byte text stub. If that stub is present at the archived DMG path, bootstrap hashes it, fails `dmgSha256`, and **does not** fall back to `dmgUrl`. Run `git lfs pull` before `npm run bootstrap` or `npm test`.
</Warning>

## Archive layout

:::files
research-archives/original/0.18.0/
├── artifacts.json
├── SHA256SUMS
├── macos-arm64/Grok_Bot_0.18.0.dmg
└── windows-x64/Grok_Bot_0.18.0_Setup.exe
:::

`.gitattributes` tracks only the binaries:

```text
research-archives/original/**/*.dmg filter=lfs diff=lfs merge=lfs -text
research-archives/original/**/*.exe filter=lfs diff=lfs merge=lfs -text
```

`artifacts.json` and `SHA256SUMS` stay ordinary Git text. `NOTICE.md` treats the installers as subject to their own terms; they are not covered by any license applied to reconstructed code.

```text
Git LFS archive                         Reconstruction toolchain
─────────────────                       ────────────────────────
macos-arm64/*.dmg  ──SHA-256 pin──►     archivedDmg → cachedDmg
                                        → Grok Bot.app → src/app/dist
windows-x64/*.exe  ──inventory only──►  not used by bootstrap/package
artifacts.json     ──schemaVersion 1──► tests/research-archives.test.mjs
SHA256SUMS         ──shasum -c────────► operator verification
```

## Pinned identities

| Platform | Architecture | Path | Bytes | SHA-256 | `sourceUrl` |
| --- | --- | --- | ---: | --- | --- |
| `darwin` | `arm64` | `macos-arm64/Grok_Bot_0.18.0.dmg` | 155793020 | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` | `https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg` |
| `win32` | `x64` | `windows-x64/Grok_Bot_0.18.0_Setup.exe` | 125825552 | `464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e` | `https://downloads.cursor.com/grokbot/stable/win32-x64/0.18.0/Grok_Bot_0.18.0_Setup.exe` |

The macOS digest is the same constant as `dmgSha256`. `PROVENANCE.md` records that the URLs came from browser download metadata on the archived local copies. The extracted `app.asar` pin (`upstreamAsarSha256`) is a downstream identity of the DMG contents, not a third installer.

## `artifacts.json` schema

The test in `tests/research-archives.test.mjs` requires exactly these top-level keys and two artifacts. Extra keys fail.

<ParamField body="schemaVersion" type="number" required>
Must be `1`.
</ParamField>

<ParamField body="product" type="string" required>
Must be `Grok Bot`.
</ParamField>

<ParamField body="version" type="string" required>
Must be `0.18.0`.
</ParamField>

<ParamField body="artifacts" type="array" required>
Exactly two objects. Each object has only `platform`, `architecture`, `path`, `bytes`, `sha256`, and `sourceUrl`.
</ParamField>

Per-artifact constraints enforced by the same test:

| Field | Constraint |
| --- | --- |
| `path` | `macos-arm64/<name>.dmg` or `windows-x64/<name>.exe` (no extra `/`) |
| `sha256` | lowercase 64 hex |
| `sourceUrl` | `https://downloads.cursor.com/grokbot/stable/...` |
| `bytes` | on-disk file size; mismatch message is `<path> requires git lfs pull` |
| file type | regular file, not a symlink, resolved under `research-archives/original/0.18.0/` |

<RequestExample>
```json artifacts.json
{
  "schemaVersion": 1,
  "product": "Grok Bot",
  "version": "0.18.0",
  "artifacts": [
    {
      "platform": "darwin",
      "architecture": "arm64",
      "path": "macos-arm64/Grok_Bot_0.18.0.dmg",
      "bytes": 155793020,
      "sha256": "a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb",
      "sourceUrl": "https://downloads.cursor.com/grokbot/stable/darwin-arm64/0.18.0/Grok_Bot_0.18.0.dmg"
    },
    {
      "platform": "win32",
      "architecture": "x64",
      "path": "windows-x64/Grok_Bot_0.18.0_Setup.exe",
      "bytes": 125825552,
      "sha256": "464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e",
      "sourceUrl": "https://downloads.cursor.com/grokbot/stable/win32-x64/0.18.0/Grok_Bot_0.18.0_Setup.exe"
    }
  ]
}
```
</RequestExample>

## Fetch and verify

<Steps>
<Step title="Install LFS and materialize objects">

```sh
git lfs install
git lfs pull
git lfs ls-files
```

`git lfs ls-files` must list both:

- `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg`
- `research-archives/original/0.18.0/windows-x64/Grok_Bot_0.18.0_Setup.exe`

</Step>
<Step title="Reject pointer stubs">

A materialized object is ~155 MB (DMG) or ~126 MB (setup.exe). A pointer looks like:

```text
version https://git-lfs.github.com/spec/v1
oid sha256:a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb
size 155793020
```

The pointer `oid` equals the full-object SHA-256; the pointer file itself does not.

</Step>
<Step title="Check SHA256SUMS">

```sh
cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS
```

`SHA256SUMS` is two GNU-style lines (hash, two spaces, relative path):

```text
a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb  macos-arm64/Grok_Bot_0.18.0.dmg
464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e  windows-x64/Grok_Bot_0.18.0_Setup.exe
```

</Step>
<Step title="Run the inventory test">

```sh
node --test tests/research-archives.test.mjs
```

That test hashes both files and asserts `lstat.size === artifact.bytes`. `npm test` includes this glob via `tests/*.test.mjs`.

</Step>
</Steps>

<ResponseExample>
```text
macos-arm64/Grok_Bot_0.18.0.dmg: OK
windows-x64/Grok_Bot_0.18.0_Setup.exe: OK
```
</ResponseExample>

## Bootstrap consumption of the archived DMG

`scripts/lib/config.mjs` binds:

| Constant | Value |
| --- | --- |
| `archivedDmg` | `research-archives/original/0.18.0/macos-arm64/Grok_Bot_0.18.0.dmg` |
| `cachedDmg` | `.cache/downloads/Grok_Bot_0.18.0.dmg` |
| `dmgSha256` | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| `dmgUrl` | public darwin-arm64 0.18.0 URL above |

`scripts/bootstrap-runtime.mjs` `downloadDmg()` runs only when neither `GROK_BOT_018_APP` nor a valid `.cache/runtime/Grok Bot.app` is used. Order inside `downloadDmg()`:

1. Reuse `cachedDmg` when its SHA-256 is `dmgSha256`.
2. If `archivedDmg` exists, hash it. Mismatch throws `Archived DMG checksum mismatch: expected <dmgSha256>, got <digest>. Run git lfs pull before bootstrapping.` Match copies the file to `cachedDmg` and returns (`Using archived release <archivedDmg>`).
3. Otherwise `fetch(dmgUrl)` into `cachedDmg.partial`, hash, rename.

The research-archives test asserts that `await copyFile(archivedDmg, cachedDmg)` appears in source **before** `await fetch(dmgUrl)`.

The Windows installer has no `archived*` constant and is not copied into `.cache`.

## Publication and clone pitfalls

Before a public remote, `docs/PUBLISHING.md` requires Git LFS at first `git add`, then `git lfs push --all origin`, then `git lfs ls-files` showing both 0.18.0 installers. Hosting ZIP/tarball downloads that omit LFS objects ship pointer stubs only.

`git archive` used by `npm run publication:check` stores whatever is in the Git tree for those paths (typically LFS pointers unless the archive step smudges objects). A fresh clone still needs `git lfs pull` before checksum tests or bootstrap-from-archive.

Do not commit reconstructed `dist/` apps or extracted `src/app/dist` as substitutes for these installers.

## Failure signals

| Symptom | Cause | Action |
| --- | --- | --- |
| `… requires git lfs pull` from the archives test | On-disk size ≠ `bytes` (usually a 134-byte pointer) | `git lfs install && git lfs pull` |
| `Archived DMG checksum mismatch` | Pointer or corrupt file at `archivedDmg` | Pull LFS objects; do not rely on `dmgUrl` while the stub exists |
| `shasum -c` FAILED | Content ≠ `SHA256SUMS` | Re-pull; do not edit `SHA256SUMS` to match a stub |
| Hosting ZIP contains `version https://git-lfs.github.com/spec/v1` | Archive without LFS objects | Enable LFS in source archives, or clone + `git lfs pull` |
| Bootstrap never sees the archive | `GROK_BOT_018_APP` set or valid cached app already present | Expected; archive is unused in those modes |

## Next

<CardGroup>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
How `npm run bootstrap` orders `GROK_BOT_018_APP`, cached app, LFS DMG, then `dmgUrl`.
</Card>
<Card title="Evidence and provenance" href="/evidence-and-provenance">
DMG and `app.asar` SHA-256 pins, evidence-only UI rule, production bindings.
</Card>
<Card title="Bootstrap failures" href="/bootstrap-failures">
Pointer-sized archives, DMG mismatch, missing `app.asar`, hydration checksum errors.
</Card>
<Card title="Publication export" href="/publication-export">
`publication:check`, LFS push of installers, NOTICE and rights review.
</Card>
</CardGroup>

---

## 25. Publication export

> npm run publication:check archive/init/add tree equality, LFS push of installers, and NOTICE/rights review before a public remote.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/25-publication-export.md
- Generated: 2026-08-24T05:59:29.914Z

### Source Files

- `docs/PUBLISHING.md`
- `scripts/verify-publication-tree.mjs`
- `NOTICE.md`
- `CONTRIBUTING.md`
- `tests/publication-bootstrap.test.mjs`
- `package.json`

---
title: "Publication export"
description: "npm run publication:check archive/init/add tree equality, LFS push of installers, and NOTICE/rights review before a public remote."
---

`npm run publication:check` runs `node scripts/verify-publication-tree.mjs`. The script archives the current `HEAD` tree, extracts it into a scratch directory, `git init`s a new repository, `git add --all`, then requires `git write-tree` in that export to equal `git rev-parse HEAD^{tree}` in the source checkout. That is the lossless fresh-history proof. It does not push a remote, does not install or fetch Git LFS objects, and does not review `NOTICE.md`. Those remain separate gates before a public remote.

<Warning>
Do not push an existing “cleaned” branch and assume deleted recovery files are gone. Parent commits can still contain generated recovery material. Export a **new** repository from an archive of the clean commit so the public history never includes those objects.
</Warning>

## Command

| Item | Value |
| --- | --- |
| npm script | `publication:check` |
| Implementation | `scripts/verify-publication-tree.mjs` |
| Git / tar | `/usr/bin/git`, `/usr/bin/tar` |
| Archive ref | `HEAD` (not a hardcoded branch name) |
| Scratch prefix | `$TMPDIR/grok-bot-publication-` |
| Success stdout | `Publication export preserves <N> files and tree <tree-oid>.` |

<RequestExample>
```sh
npm run publication:check
```
</RequestExample>

<ResponseExample>
```text
Publication export preserves 4123 files and tree <40-char-oid>.
```
</ResponseExample>

The file count is `git ls-tree -r --name-only HEAD` in the source repo. The tree oid is `HEAD^{tree}`. Scratch files are always removed in a `finally` block.

### What the checker does

```mermaid
flowchart LR
  subgraph source["Source checkout"]
    HEAD["HEAD tree objects"]
    hist["Existing Git history"]
    lfs["LFS object store"]
  end
  subgraph check["publication:check scratch"]
    archive["git archive --format=tar HEAD"]
    extract["tar -xf into exported/"]
    init["git init --quiet"]
    add["git add --all"]
    write["git write-tree"]
  end
  subgraph public["New public remote — not part of the checker"]
    commit["Initial reconstructed source import"]
    push["git push -u origin main"]
    lfspush["git lfs push --all origin"]
    notice["NOTICE.md + independent rights review"]
  end
  HEAD --> archive --> extract --> init --> add --> write
  write -->|must equal HEAD^{tree}| HEAD
  hist -.->|must not be pushed| public
  archive --> commit
  lfs --> lfspush
```

Concrete steps in `verify-publication-tree.mjs`:

1. `git archive --format=tar --output=<scratch>/repository.tar HEAD`
2. `mkdir` the export directory and `tar -xf` into it
3. `git init --quiet` then `git add --all` in the export (no `git lfs install` here)
4. Compare `HEAD^{tree}` to `git write-tree`
5. Compare path sets: `git ls-tree -r --name-only HEAD` vs `git ls-files` in the export
6. Assert `frontend/src/recovered/ui/sand-form-primitives.css` exists in the export and has non-zero size

`git archive` reads **tree objects**, not the working tree. Git LFS-tracked installers are stored as pointer blobs, so the checker compares pointer content, not the 155 MB / 125 MB binaries.

## History isolation

`docs/PUBLISHING.md` names `codex/clean` as a branch that removes generated recovery from its **tree** while the parent commit still contains that material. Pushing that branch (or any equivalent rewrite) leaks recovery into reachable history.

Create an empty directory and import only the archived tree:

```sh
git archive --format=tar HEAD | tar -xf - -C /path/to/empty-export
cd /path/to/empty-export
git init
git lfs install
git add .
git commit -m "Initial reconstructed source import"
```

Run `publication:check` on the **committed** clean checkout first. The script archives whatever `HEAD` is; it will not refuse a dirty-history branch.

## Git LFS installers

`.gitattributes` tracks:

- `research-archives/original/**/*.dmg filter=lfs diff=lfs merge=lfs -text`
- `research-archives/original/**/*.exe filter=lfs diff=lfs merge=lfs -text`

Pinned 0.18.0 identities (`artifacts.json` `schemaVersion` `1`):

| Path | Bytes | SHA-256 |
| --- | ---: | --- |
| `macos-arm64/Grok_Bot_0.18.0.dmg` | 155793020 | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| `windows-x64/Grok_Bot_0.18.0_Setup.exe` | 125825552 | `464079a15ef5fa8b61ccea8fffcc78f63cfcf6df65fb0ad5e725d8b95f7e437e` |

After the remote exists:

```sh
git push -u origin main
git lfs push --all origin
```

<ParamField body="git lfs push --all" type="command" required>
Pushes every LFS object referenced by the new history. A normal `git push` can leave pointer-only files on the remote if the LFS transfer is skipped.
</ParamField>

If the host offers downloadable source ZIP/tarball archives, enable inclusion of Git LFS objects. Otherwise those downloads contain only pointer files (`version https://git-lfs.github.com/spec/v1`).

On a clone that should actually bootstrap from the local DMG:

```sh
git lfs install
git lfs pull
(cd research-archives/original/0.18.0 && shasum -a 256 -c SHA256SUMS)
git lfs ls-files
```

`git lfs ls-files` must list both installers. `NOTICE.md` states those binaries remain under their own terms and are **not** covered by any license applied to reconstructed code.

## Pre-public-remote checklist

<Steps>
<Step title="Prove archive/init/add tree equality">
On the committed clean checkout:

```sh
npm run publication:check
```

A tree mismatch aborts with omitted/unexpected paths (first 20 of each).
</Step>
<Step title="Rebuild from a fresh clone or export">

```sh
npm ci
npm run bootstrap
npm run check
npm run package
npm run verify
```

`package` is macOS-only. Confirm this from the export, not a dirty working tree that still has `.cache/` or `src/app/dist/`.
</Step>
<Step title="Confirm generated payloads stay untracked">

```sh
git status --ignored
```

Ignored roots include `node_modules/`, `/.cache/`, `/.build/`, `/dist/`, `/src/app/dist/`, `/recovered/`, `/recovery/`, `/reports/`, `/work/`. None of those should be staged.
</Step>
<Step title="Confirm LFS installers are present">

```sh
git lfs ls-files
```

Expect both `Grok_Bot_0.18.0.dmg` and `Grok_Bot_0.18.0_Setup.exe`.
</Step>
<Step title="Scan the new history">
Search the exported tree and the new repository’s full history for credentials and absolute machine paths. `publication:check` does not do this.
</Step>
<Step title="Review NOTICE.md and rights">
Obtain an independent copyright, trademark, dependency, and service-terms review. Decide a license only for material you have authority to license. Do not imply that license covers the upstream Grok Bot application, trademarks, or the LFS installers. This repository ships `NOTICE.md` and no root `LICENSE`.
</Step>
</Steps>

## NOTICE and rights

`NOTICE.md` is the redistribution warning that must stay in the export:

- Unofficial reconstruction; not affiliated with or endorsed by Anysphere, Cursor, xAI, or SpaceX.
- No upstream source-code license is asserted or granted.
- Removing the original binary payload and recovery evidence from Git does **not** make redistribution safe.
- Git LFS 0.18.0 macOS and Windows installers are preservation copies under their own terms.

`PROVENANCE.md` repeats: complete an independent rights review before public redistribution; do not present reconstructed material as original source or an official build.

`SECURITY.md` asks for private reports to the repository owner rather than public disclosure. Publication cleanup must not silently change reconstructed runtime contracts to clear `npm audit` findings.

`CONTRIBUTING.md` forbids weakening checksum, bundle identity, code-signing, or **clean-export** checks to make a build pass.

## Ignore rules the export must keep

`.gitignore` uses a **rooted** `/recovered/` rule so root forensic output stays out of Git while `frontend/src/recovered/` remains addable.

| Path | Expected |
| --- | --- |
| `frontend/src/recovered/ui/sand-form-primitives.css` | Tracked, non-empty in the archive; `publication:check` fails if omitted or zero-length |
| `recovered/` at repo root | Ignored generated recovery |
| `src/app/dist/` | Ignored hydration output |
| `dist/`, `.build/`, `.cache/` | Ignored package/bootstrap output |

`tests/publication-packaging.test.mjs` asserts `/recovered/` (not a bare `recovered/`) and that `ignore()` does not drop the frontend CSS path.

Checked-in UI catalogs under `frontend/manifests/` remain live build inputs. Historical recovery registries whose `anchor.registry` starts with `recovered/` are treated as omitted from the clean publication tree.

`tests/publication-bootstrap.test.mjs` additionally requires production bindings in `manifests/reconstruction/electron-main-production-bindings-manifest.json` to resolve only under `source/` (`module` matching `^\.\./\.\./source/`). That invariant belongs in any public tree even though `publication:check` does not parse the manifest.

## Failures

| Symptom | Cause | Fix |
| --- | --- | --- |
| `Fresh publication export changed the tracked tree. Omitted: …` | `.gitignore` (or export-only files) dropped tracked paths after `git add --all` | Keep rooted ignore rules; do not add a bare `recovered/` pattern |
| `Unexpected: …` | Export index contains paths not in `HEAD` | Stop generating files that `git add` would pick up in a fresh repo |
| `Fresh publication export omitted frontend/src/recovered/ui/sand-form-primitives.css` | File missing or empty after archive extract | Restore the tracked reconstructed CSS |
| `git` / `tar` non-zero exit | `/usr/bin/git` or `/usr/bin/tar` missing or archive failed | Run on a machine with those tools; ensure `HEAD` exists |
| Remote ZIP is 134-byte LFS pointers | Host archive omitted LFS objects, or `git lfs push` was skipped | `git lfs push --all origin`; enable LFS-in-archives on the host |
| Clone bootstrap fails on DMG size | Working copy still has pointer files | `git lfs pull` then `shasum -a 256 -c SHA256SUMS` |
| Public history still has `/recovered/` payloads | Pushed a cleaned branch instead of a new-history import | Re-export with `git archive` into a new empty repository |

Do not “fix” a failed check by deleting `publication:check`, loosening `.gitignore` tests, or dropping LFS attributes.

## Next

<CardGroup>
<Card title="Preserve original installers" href="/preserve-original-installers">
LFS DMG and setup.exe identities, `artifacts.json` schemaVersion 1, and `shasum -c SHA256SUMS`.
</Card>
<Card title="npm scripts" href="/npm-scripts">
Exact `package.json` commands including `publication:check`, `check`, `package`, and `verify`.
</Card>
<Card title="Contributing" href="/contributing">
Required check/package commands and the rule not to weaken clean-export gates.
</Card>
<Card title="Evidence and provenance" href="/evidence-and-provenance">
Pinned 0.18.0 checksums and the evidence-only reconstruction boundary that a public tree must keep.
</Card>
<Card title="Bootstrap the pinned runtime" href="/bootstrap-pinned-runtime">
How a fresh clone turns the LFS DMG into a verified `src/app/dist` payload.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
`npm run verify` required ASAR paths and `npm run smoke` after a publication rebuild.
</Card>
</CardGroup>

---

## 26. Contributing

> Required check and package commands, no generated payloads in Git, do not weaken checksum or signing gates, private security reports.

- Page Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/pages/26-contributing.md
- Generated: 2026-08-24T05:59:03.930Z

### Source Files

- `CONTRIBUTING.md`
- `SECURITY.md`
- `NOTICE.md`
- `PROVENANCE.md`
- `package.json`
- `tests/publication-packaging.test.mjs`

---
title: "Contributing"
description: "Required check and package commands, no generated payloads in Git, do not weaken checksum or signing gates, private security reports."
---

This repository is a small-club reconstruction. A shareable change must pass `npm ci`, `npm run check`, and `npm run frontend:build`. After `npm run bootstrap` on macOS Apple Silicon, package changes must also pass `npm run package` and `npm run verify`. `check` is `typecheck` (`frontend/tsconfig.json`) plus `source:typecheck` (`source/tsconfig.json`) plus `node --test tests/*.test.mjs`. `package` re-runs `check`, then `scripts/package-macos.mjs`. Node is pinned to `>=26.5.0 <27`. Do not commit generated application payloads or local evidence, and do not weaken checksum, bundle identity, code-signing, or clean-export checks to make a build pass.

<Warning>
This is not a supported production distribution. Do not reuse real credentials or sensitive accounts while experimenting. Report issues privately to the repository owner; do not open a public disclosure against this experimental codebase.
</Warning>

## Required commands

<Steps>
<Step title="Install a clean tree">
Use Node 26.5.x. From the repository root:

```sh
npm ci
```

`postinstall` applies `scripts/apply-third-party-patches.mjs`. Do not skip `npm ci` for a shareable change.
</Step>
<Step title="Run the share gate">
```sh
npm run check
npm run frontend:build
```

`check` fails if frontend TypeScript, reconstructed runtime TypeScript, or any `tests/*.test.mjs` file fails. `frontend:build` is `vite build --config frontend/vite.config.ts` and is required even though the packaged UI still uses the patched shipped renderer by default.
</Step>
<Step title="On macOS, package and verify">
After a successful `npm run bootstrap`:

```sh
npm run package
npm run verify
```

`package` aborts on non-Darwin with `The reconstructed macOS application can only be packaged on macOS.` Output is `dist/Grok Bot 0.18 Reconstructed.app` unless `GROK_BOT_OUTPUT_APP_NAME` overrides the basename. `verify` inspects that bundle (or `--app /absolute/path/to/App.app`), not `.build/app.asar`.
</Step>
</Steps>

<RequestExample>
```sh title="Share a change"
npm ci
npm run check
npm run frontend:build
```
</RequestExample>

<RequestExample>
```sh title="macOS package gate"
npm run bootstrap
npm run package
npm run verify
```
</RequestExample>

Optional extra commands after a packaged app exists: `npm run smoke` (native e2e, 12s timeout) and `npm run publication:check` (archive/init/add tree equality on `HEAD`). Those are not substitutes for `check` / `frontend:build` / `package` / `verify`.

| Script | What it runs | Required before sharing |
| --- | --- | --- |
| `npm run check` | `typecheck` + `source:typecheck` + `test` | Yes |
| `npm run frontend:build` | Vite build of `frontend/` | Yes |
| `npm run package` | `check` then `scripts/package-macos.mjs` | Yes on macOS for packaging changes |
| `npm run verify` | `scripts/verify.mjs` against the selected `.app` | Yes on macOS after `package` |
| `npm test` | `node --test tests/*.test.mjs` | Covered by `check` |
| `npm run smoke` | `scripts/smoke.mjs`, `timeoutMs: 12_000` | Optional native launch check |
| `npm run publication:check` | `scripts/verify-publication-tree.mjs` | Before a public remote / clean export |

## Classify the change

Use focused commits. State which surface moved:

| Surface | Typical paths | Packaged effect |
| --- | --- | --- |
| Reviewed runtime source | `source/` | Compiled into the reconstructed ASAR (`clean-source` runtimes) |
| Editable frontend | `frontend/` | Vite design workspace; default `package` does **not** replace the shipped renderer |
| Checksum-pinned packaged renderer | Hydrated `src/app/dist/renderer` plus `scripts/lib/router-renderer-patch.mjs` | Default UI authority; only a narrow, hash-recorded Router transform is applied |
| Packaging only | `scripts/package-macos.mjs`, `scripts/verify.mjs`, `scripts/lib/codesign.mjs` | Bundle identity, ASAR swap, ad-hoc sign, verify |

Passing `frontend:build` alone does not prove the packaged UI changed. Default packaging calls `buildFidelityReconstructedAsar()` and keeps the checksum-pinned shipped renderer. Override only with an explicit `GROK_BOT_RENDERER_SOURCE` directory that contains `index.html`.

## Keep generated payloads out of Git

The checked-in tree is reconstructed source, tests, manifests, build scripts, and Git LFS original 0.18.0 installers. Bootstrap hydrates ignored `src/app/dist`. Package writes ignored `.build/` and `dist/`.

`.gitignore` (root-anchored) includes:

| Pattern | Why it stays out of Git |
| --- | --- |
| `/.cache/` | Bootstrap DMG / runtime cache |
| `/.build/` | Staged ASAR and package scratch |
| `/dist/` | `Grok Bot 0.18 Reconstructed.app` |
| `/src/app/dist/` | Checksum-pinned hydrated payload |
| `/recovered/` | Root recovery output (not `frontend/src/recovered/`) |
| `/recovery/`, `/reports/`, `/work/` | Local evidence and scratch |
| `/.tmp*/`, `/.grok-*/`, `/.playwright-cli/`, `/.http-probe.*/`, `/.mcp-bridge.*/`, `/.openrouter-probe.*/`, `/.router-*/` | Machine-local harnesses |
| `/host-main.cjs`, `/main.cjs`, `/index-*.js`, `/katex-*.js`, `/mermaid.core-*.js`, `/xlsx-*.js`, `/index.html` | One-off extracted copies at repo root |

`tests/publication-packaging.test.mjs` requires `^/recovered/$` (root only) and that `frontend/src/recovered/ui/sand-form-primitives.css` remains addable. Do not “fix” publication by ignoring `frontend/src/recovered/`.

Before a public remote, `npm run publication:check` archives `HEAD`, `git init`s a fresh tree, `git add --all`, and requires `HEAD^{tree}` to equal the new `write-tree`. Mismatch throws `Fresh publication export changed the tracked tree`. Confirm `git status --ignored` shows no generated payload selected for Git.

## Do not weaken gates

Do not patch these checks to green a PR.

### Checksums

| Pin | Value / owner |
| --- | --- |
| 0.18.0 DMG SHA-256 | `a253ccd8aab01e083f9812a0264354c5034d8ba7f0610bbb557e82ae77d203eb` |
| Original `app.asar` SHA-256 | `6665408168466f9cacc6087e917890c17f59d2e2e9c2404a5c4a59ad79c1de58` |
| Renderer mode | Default packaged renderer is `checksum-pinned-artifact-runtime`; `verify` byte-compares the inventory |

Bootstrap fails on LFS pointer-sized archives and on `checksum mismatch`. `verify` fails if a packaged renderer file’s bytes or SHA-256 drift from provenance, or if `ElectronAsarIntegrity` remains in `Info.plist`.

### Bundle identity

`scripts/lib/config.mjs` sets `reconstructedBundleId` to `com.anysphere.sand.reconstructed` and `reconstructedName` to `Grok Bot 0.18 Reconstructed`. `package` writes those via `plutil` and registers URL scheme `sand` only. `verify` throws `Unexpected reconstructed bundle ID` or `Unexpected reconstructed display name` on drift. Do not restore `com.anysphere.sand` or claim the upstream Developer ID / notarization.

### Code signing

Packaging clears `_CodeSignature`, ad-hoc signs with identity `-` (`--force --deep --timestamp=none --sign -`), retries once on transient nested-framework failure, then runs `codesign --verify --deep --strict`. `verify` repeats that verify. Quarantine is stripped with `xattr -cr` so Gatekeeper does not reject a valid ad-hoc signature. Do not skip sign or verify to paper over a dirty payload.

### Updater and telemetry guard

Reconstructed Electron-main packaging prepends an idempotent guard:

```js
process.env.SAND_DISABLE_UPDATES ??= "1";
process.env.SAND_DISABLE_SENTRY ??= "1";
process.env.SAND_DISABLE_TELEMETRY ??= "1";
```

`tests/reconstructed-updater-guard.test.mjs` locks that prefix. Explicit environment still wins (`??=`). Do not remove the guard to “match production.”

### Clean export and production bindings

Checked-in production bindings must resolve under `source/`. `publication:check` and `git archive` must not drop `frontend/src/recovered`. Do not loosen ignore rules, tree-equality, or binding-path assertions to hide generated evidence.

## Frontend and evidence-only UI

`frontend/` is a partial, evidence-backed reconstruction. Packaged builds keep the pinned renderer and apply only the recorded Settings Router transform unless `GROK_BOT_RENDERER_SOURCE` overrides it.

The immutable 0.18.0 release is the product specification. Recovered source may express only behavior with an inspectable artifact anchor (emitted code or source-path markers, capsules/source maps, shipped strings/assets/CSS, renderer DOM signatures, IPC/RPC contracts, or repeatable observation). Do not invent a screen, route, control, label, selector, state, or interaction to fill a gap. Passing typecheck or Vite build is not provenance. Speculative UI is a release-blocking defect.

`@evidence` comments in `frontend/` are provenance annotations, not imports.

## Security reports

<ParamField body="channel" type="string" required>
Private report to the repository owner. Do not file a public disclosure against this experimental codebase.
</ParamField>

Reconstructed packages default the official updater, Sentry, and upstream telemetry off at the Electron-main packaging boundary. The bootstrap download and hydrated `app.asar` remain checksum-pinned.

`npm audit` still reports compatibility-bound advisories in pinned Electron 42.1, Undici 5 / Connect 1, AI SDK 4, and OpenTelemetry. Patch-level fixes are allowed only when they do not change reconstructed runtime contracts. Do not silently major-upgrade those stacks as publication cleanup.

No upstream source-code license is granted. `NOTICE.md` and `PROVENANCE.md` require an independent rights review before public redistribution. Preserved LFS installers remain under their own terms.

## Local verification signals

<Check>
`npm run check` and `npm run frontend:build` exit 0.
</Check>

<Check>
On macOS, `npm run package` prints `Packaged application: .../dist/Grok Bot 0.18 Reconstructed.app` and `npm run verify` prints `Verified packaged ASAR` plus reconstructed bundle identity and code signature.
</Check>

<Check>
`git status --ignored` does not stage `.cache/`, `.build/`, `dist/`, `src/app/dist/`, or `/recovered/`.
</Check>

Failure strings that mean a gate is working, not a license to skip it:

| Signal | Meaning |
| --- | --- |
| `The reconstructed macOS application can only be packaged on macOS.` | `package` is Darwin-only |
| `Unexpected reconstructed bundle ID` | `CFBundleIdentifier` is not `com.anysphere.sand.reconstructed` |
| `ASAR is missing …` | Required packaged paths dropped |
| `Packaged artifact renderer differs from its checksum inventory` | Renderer bytes drifted |
| `Fresh publication export changed the tracked tree` | Ignore/export rules drifted |
| `checksum mismatch` | Bootstrap pin failed |

## Next

<CardGroup>
<Card title="npm scripts" href="/npm-scripts">
Exact `package.json` commands: `bootstrap`, `check`, `package`, `verify`, `smoke`, `publication:check`, `frontend:build`.
</Card>
<Card title="Package the macOS app" href="/package-macos-app">
Compile runtimes, patch Settings Router, replace `app.asar`, set reconstructed identity, ad-hoc sign, verify.
</Card>
<Card title="Verify and smoke" href="/verify-and-smoke">
Required ASAR paths, evidence markers, native e2e with 12s timeout, `SAND_DISABLE_*` defaults.
</Card>
<Card title="Publication export" href="/publication-export">
`publication:check` tree equality, LFS push of installers, `NOTICE`/rights review.
</Card>
<Card title="Work on the frontend reconstruction" href="/edit-frontend-workspace">
`frontend/` Vite workspace versus the patched shipped renderer.
</Card>
<Card title="Evidence and provenance" href="/evidence-and-provenance">
Pinned 0.18.0 identities, evidence-only UI rule, production bindings into `source/`.
</Card>
</CardGroup>

---
