feat(grok): add Grok Build (xAI) as a seventh CLI run mode

SessionMode gains 'grok', a first-class backend alongside Claude Code,
shell, OpenCode, Codex, Gemini, Antigravity and Pi: its own PTY, tmux
session, charcoal tab identity ('gk' badge), welcome button, run-mode
entry, cron agentType, Docker and remote-SSH command defaults, and
clone-repo Brain option. Flag surface verified live against grok 1.0.5.

Grok mixes two existing shapes and the wiring follows from that:

- Codex-shaped on permissions: the bypass switch is GrokConfig.alwaysApprove
  (--always-approve, grok's bypassPermissions mode; config-level deny rules
  still apply on top). The Run button sends it true, like runAntigravity(),
  and clampExternalCliBypassForOwner() puts grok in the only-if-sent branch:
  a bare grok spawn is grok's own ask-mode default, which is already safe,
  so only a sent config needs the flag forced off. Cron needs nothing for
  the same reason.
- OpenCode-shaped on rendering: grok is a fullscreen alternate-screen TUI
  with mouse support, so it stays OUT of isAltScreenStripMode() and lands
  on the narrow tmux-attach strip and the 'buffer' local-echo fallthrough
  (unmeasured against an authenticated composer; documented fallback is the
  'off' branch).
- Pi-shaped on resolution: 'grok' has npm squatters (@vibe-kit/grok-cli
  also installs a grok bin), so grok-cli-resolver.ts version-probes every
  candidate (grok --version, killSignal SIGKILL, VITEST-gated) and
  GET /api/grok/status surfaces path AND version; GROK_VERSION_REGEX is
  shared with the dependency registry so doctor and run mode cannot drift.

Env allowlist gains GROK_* plus the XAI_* vendor namespace (XAI_API_KEY is
grok's documented headless auth var), the same narrow-vendor reasoning as
GOOGLE_* for gemini. Resume is id-regexed on purpose: grok's own --resume
also matches session titles, which are arbitrary user strings that must
never reach the bash -c spawn line.

Docker: grok is not on npm, so the agent image installs it in its own step
(xAI's installer has no --dir override; the binary is copied to
/usr/local/bin and root's ~/.grok dropped in the same layer), and
credentials are seeded per-file (auth.json, config.toml, pager.toml; the
dir also holds sessions/, memory/ and the ~160MB binary). Remote SSH routes
through the login-shell wrapper like the other agent CLIs.

Verified end to end on an isolated CODEMAN_INSTANCE with grok 1.0.5
installed: /api/grok/status resolves and reports the probed version,
quick-start spawns a pane whose command line ends in 'grok
--always-approve', the real TUI renders (OAuth device screen on an
unauthenticated box), and grokConfig round-trips through state.json.
Docs: docs/grok-integration.md (user guide) + docs/grok-integration-plan.md
(decisions, verification record, follow-ups).

Tests: test/grok-mode.test.ts, test/grok-cli-resolver.test.ts, plus
extended clamp/system-routes/render-index-html/run-mode-ui/mobile-overview/
local-echo-gating coverage. npm test (the CI gate) green: 5910 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-23 08:39:03 +02:00
parent 88bb98de43
commit 3f8c8e99d1
55 changed files with 1474 additions and 119 deletions
+133
View File
@@ -0,0 +1,133 @@
# Grok Build (xAI) sessions
Codeman can drive [Grok Build](https://github.com/xai-org/grok-build) (xAI's `grok`
CLI, the agent behind docs.x.ai/build) as a session backend, alongside Claude Code,
OpenCode, Codex, Gemini, Antigravity and Pi. `grok` is a seventh **run mode**: its own
PTY, its own tmux session, its own tab identity (monochrome charcoal, `gk` badge). It
is not a location overlay like Docker or remote-SSH cases, and it is not a web tab.
The design rationale behind each decision below lives in
[`grok-integration-plan.md`](./grok-integration-plan.md). Everything here was verified
against grok 1.0.5.
## Install
```bash
curl -fsSL https://x.ai/cli/install.sh | bash
```
The installer places the binary in `~/.grok/bin` and symlinks it into `~/.local/bin`
(it also installs an `agent` alias Codeman ignores). `grok update` self-updates.
Codeman resolves the binary via the server PATH and then the usual install locations,
`~/.grok/bin` first. **`grok` is a name with known squatters** (the unrelated
`@vibe-kit/grok-cli` npm package also installs a `grok` bin), so like `pi` the
resolver does not trust a PATH hit on its own: it runs `grok --version` once and
requires version-shaped output (`grok 1.0.5 (5115b46bc9)`). Check what it resolved:
```bash
curl -s localhost:3000/api/grok/status | jq
# { "available": true, "path": "/home/you/.grok/bin", "version": "1.0.5" }
```
The endpoint carries `version` on top of the sibling `/api/*/status` shape precisely
so a misresolution is visible rather than presenting as "the mode just doesn't work".
## Authenticate
- **Browser OAuth (default)**: the first `grok` run opens a sign-in flow; in a
Codeman pane you get the device-code screen with a URL to open elsewhere.
Credentials land in `~/.grok/auth.json` (0600) and refresh automatically.
- **Device code**: `grok login --device-auth`, made for SSH boxes and headless hosts.
- **API key**: `export XAI_API_KEY="xai-..."` (console.x.ai). Used as a fallback when
no session token exists. As a per-session Codeman `envOverride` it flows through
socket-scoped `tmux setenv`, never the spawn command line.
- **Enterprise OIDC**: `GROK_OIDC_ISSUER` / `GROK_OIDC_CLIENT_ID`.
## What Codeman wires up
`GrokConfig` (per session, persisted in `state.json`, round-trips through respawn):
| Field | Flag | Notes |
| ----------------- | --------------------------- | --------------------------------------------------------------------- |
| `model` | `--model <v>` | e.g. `grok-4.5`, or a custom `[model.<name>]` from `config.toml` |
| `alwaysApprove` | `--always-approve` | Grok's `bypassPermissions` mode; deny rules still apply on top |
| `continueSession` | `--continue` | Most recent session for the working directory; skipped when resuming |
| `resumeSessionId` | `--resume <v>` | Ids only, never titles (grok's own `--resume` also matches titles) |
Every value is regex-validated and **dropped** (not escaped) if it fails, because the
result is interpolated into the pane's `bash -c "..."` command.
The Run button sends `grokConfig: { alwaysApprove: true }`, the same product decision
as Claude's `--dangerously-skip-permissions` default and Antigravity's
`--dangerously-skip-permissions`: Codeman sessions exist for autonomous work. Keep
hard limits as `deny` rules in `~/.grok/config.toml` (they apply in every mode), and
in **multi-user mode** a non-granted owner's `alwaysApprove` is forced off
server-side; a bare `grok` spawn is grok's own ask-mode default.
Env overrides: the `GROK_*` prefix (`GROK_HOME`, `GROK_CONFIG`, `GROK_MEMORY`,
`GROK_WORKFLOWS`, `GROK_SANDBOX`, `GROK_OIDC_*`, ...) plus the `XAI_*` vendor
namespace (`XAI_API_KEY`) are allowlisted. Foreign provider keys are not, as ever.
## What Codeman deliberately does NOT wire up
- **`--permission-mode`, `--allow`/`--deny`.** The boolean covers the autonomous
case; the full rule surface is a follow-up with UI.
- **`-p`/headless, `--output-format`, `--json-schema`.** Codeman drives the TUI.
- **`--worktree`, `--sandbox`, `--reasoning-effort`, `-s/--session-id`,
`--fork-session`, `--agent`/`--agents`.** Tracked as follow-ups in the plan doc.
## Terminal behavior
Grok renders a **fullscreen alternate-screen TUI** (scrollback pane + prompt, mouse
supported). Under Codeman it runs inside tmux like every external CLI, so the
fullscreen rendering stays inside the pane and the browser terminal shows tmux's
repaints; grok stays out of the alt-screen strip list on purpose (the opencode case,
not the Ink case). If a pane renders oddly, `grok doctor` checks terminal, color and
input support without starting a session, and `~/.grok/pager.toml` can force
`alt_screen = "inline"`.
On touch devices grok currently gets the buffered local-echo overlay like Claude,
Gemini, OpenCode and Pi. This is the fallthrough default and has not been measured
against an authenticated grok composer; if grok turns out per-keystroke reactive the
way codex was (issues #218/#219/#220/#222), the fix is the `'off'` branch in
`_updateLocalEchoState` (terminal-ui.js).
## Docker cases
The agent image installs grok in its own Dockerfile step (not npm; xAI's installer
targets `$HOME/.grok/bin` with no `--dir` override, so the binary is copied to
`/usr/local/bin`). Rebuild with the mandatory `--no-cache`:
```bash
node scripts/build-agent-image.mjs --no-cache
```
Credentials are **seeded**, not shared: `auth.json`, `config.toml` and `pager.toml`
are copied into the container's own `~/.grok`, so an in-container grok never writes
refreshed OAuth tokens back to the host and `docker commit` exports stay secret-free.
Only those three files, because `~/.grok` also holds `sessions/`, `memory/` and the
~160MB binary under `downloads/`. Trade-off, same as pi: in-container sessions are
invisible host-side, so `grok -c` inside a Docker case only sees that container's own
history.
## Remote SSH cases
`grok` mode is routed through an interactive login shell
(`exec "$SHELL" -i -l -c 'grok'`), because sshd's remote-command PATH does not include
`~/.grok/bin`. Per-session config and `envOverrides` do not cross ssh and are rejected
rather than silently ignored; use the per-host command override instead. For auth on
the remote host, `grok login --device-auth` exists for exactly this.
## Known gaps
- **No idle/completion hook yet.** Idle detection falls back to output-stabilization
like the other external CLIs. Grok has a hooks system, so a Codeman hook POSTing to
`/api/hook-event` is the highest-value follow-up.
- **No response viewer.** Grok writes ACP JSONL sessions under
`~/.grok/sessions/<encoded-cwd>/<session-id>/updates.jsonl`; nothing reads them yet.
- **Cron jobs mis-detect readiness.** The readiness poll looks for `❯` or a token
count, neither of which grok prints, so a grok cron job burns its poll budget and
then sends the prompt anyway. It works; it is just slower to start.
- **Ralph, respawn heuristics, token/CLI-info parsing and the `❯` readiness probe are
off** for grok, as for every external CLI.