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

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

## Source Files

- `source/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>
