Replace the streaming-json CLI + generated PreToolUse-hook Grok adapter with grok's native ACP mode (JSON-RPC 2.0 over stdio). Adds GrokACPBackend/GrokACPDecoder/GrokACPDecisionMapping (a near-twin of the Codex app-server adapter); generalizes CodexJSONRPC into a shared JSONRPCConnection (jsonrpc-header flag, default off so Codex is byte-identical, plus endNotifications); deletes GrokBuildBackend, GrokStreamDecoder, GrokHookConfig and strips the /grok-hook HTTP bridge from MCPApprovalServer. Claude and Codex are left native. Preserves conflict-lock parity, native resume (session/load), and the hermetic fake-grok contract test. Full suite: 525 tests green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
24 KiB
Nucleic — Grok Build Adapter (Implementation Plan, v0)
Plan for adding Grok Build (xAI's terminal coding agent) as a third AgentBackend
alongside Claude Code and the (stubbed) Codex adapters. This is a planning doc, not a shipped
adapter; it follows the same structure and rigor as ADAPTERS and is meant to be
the spec the implementation works against.
Confidence key: ✅ documented/stable · 🟡 observed/SDK-derived, pin before shipping · 🔴 inferred, a live-capture spike must record a real sample (the M0 methodology — see M0_RESULTS). Treat 🔴/🟡 as the validation backlog, not as fact.
⚑ Update (2026-06-21): shipped on ACP, not streaming-json + hooks
This doc was written for
grok -p --output-format streaming-jsonwith a generated PreToolUse hook bridging approvals over HTTP (Tier A, below). That design has been replaced. Grok now ships [agentmode —grok agent stdio— which is ACP (the Agent Client Protocol): standards JSON-RPC 2.0 over stdio, baked into the binary]. So the Grok backend is now a near-twin of the Codex app-server adapter, not of Claude:
- Transport: the shared
JSONRPCConnection(the same one Codex uses; ACP setsincludeVersionHeader: true). No more streaming-json decoder.- Approvals: native
session/request_permissionserver→client requests — the entire/grok-hookHTTP bridge, the generated.grok/PreToolUse script, and the inferred streaming-json event vocab are gone. No more 🔴-inferred wire format here; ACP is a published schema.- Implemented in
Sources/NucleicCore/Grok/:GrokACPBackend,GrokACPDecoder(session/update→AgentEvent),GrokACPDecisionMapping(Decision→ ACP permission outcome). Tests:GrokACPDecoderTests,FakeGrokContractTests(a full ACP round-trip against thefake-grokstub); fixtures underfixtures/grok/acp/.The §3 mapping table, §4 approval tier, and §6.1 status below are updated to ACP; the rest of the doc is retained as historical context for the original streaming-json plan.
0. Verdict — clean fit
The codebase was designed for this. There is a documented
AgentBackend protocol, a BackendID enum that already
anticipates multiple agents
(claudeCode/codex/codexExec), a
per-session BackendFactory seam in AppStore, per-project defaultBackend
(Project.swift), and an ADAPTERS doc whose
thesis is "wildly different wire formats converge by design." Codex is already stubbed in the enum
and Settings as "coming soon"; Grok follows the same paved path.
Grok Build is unusually convenient because it deliberately mirrors Claude Code's CLI surface
(grok -p --output-format streaming-json, --permission-mode default|plan|acceptEdits|…, MCP
servers, PreToolUse hooks, reads CLAUDE.md natively, --session resume). The shape of the
integration is therefore nearly identical to ClaudeCodeBackend.
The differences are in the wire details — and those details are the validation backlog (§2).
The net-new code is small and localized: one stream decoder + one backend actor + one approval endpoint + UI/catalog/factory glue. The hard infrastructure (process host, approval suspension, conflict locks, containers, sync, persistence, transcripts) is backend-agnostic and reused unchanged.
1. What Grok Build provides (confirmed from xAI + community docs)
- ✅ Headless:
grok -p "<prompt>" --output-format streaming-json— runs once, prints, exits nonzero on refusal/tool failure. - ✅ Permission modes:
--permission-mode {default, dontAsk, acceptEdits, bypassPermissions, plan}— the same vocabulary as Claude Code;[ui] permission_mode = ask|always-approvein~/.grok/config.tomlfor persistent use. - ✅ PreToolUse hooks (
.grok/hooks/pre_tool.sh,.grok/hooks.json): execute at agent lifecycle events, receive event JSON on stdin and return JSON on stdout, and can block/deny a tool call — even when auto-approve is on. This is the interactive-approval seam. - ✅ Tool-gating order: PreToolUse hooks → policy rules (deny > ask > allow) → built-in fast paths → prompt policy.
- ✅ MCP server support (
/mcps, config) so the agent can call internal APIs, not just shell. - ✅ Auth:
XAI_API_KEY=xai-…env, orgrok auth login(browser OAuth for SuperGrok); config under~/.grok(config.toml). - ✅ Resume: conversations persist;
--session latest/--session <id>. - ✅ Reads existing
CLAUDE.md(andAGENTS.md) project context natively — migration-friendly. - 🟡 streaming-json event vocab is xAI's own, observed as
session.start,model.thinking,tool.call,tool.result,model.message,session.end— not Anthropic's{"type":"assistant","message":{content:[…]}}envelope. Exact per-event fields must be pinned.
Sources (captured 2026-06-20): xAI "Introducing Grok Build", Mervin Praison "Grok Build CLI", aimadetools "Grok Build Complete Guide", ContentBuffer "Headless Agentic Coding", xAI Enterprise Docs.
2. Validation backlog (🔴 — a live capture must settle these before adapter code)
The same five unknowns that the M0 spike settled for Claude, restated for Grok:
- streaming-json line schemas — the exact field names inside each event
(
session.start/model.message/model.thinking/tool.call/tool.result/session.end), tool-call id + input shape, file-change signal, token-usage and cost reporting, and whether partial/delta events exist (the equivalent of Claude's--include-partial-messages). - PreToolUse hook wire contract — the exact JSON Grok writes to the hook's stdin, and the exact JSON it expects back on stdout to allow / deny / (possibly) modify a tool call. This is the single most important unknown. It decides whether we get full interactive approvals (Tier A, §4) or fall back to a fixed policy (Tier B).
- stdin follow-ups — whether headless
grok -paccepts streamed follow-up user turns on stdin (Claude's--input-format stream-json) or is strictly single-shot-per-process. (Claude is run single-shot-per-turn today and resumed via--resume; Grok will likely use the same model regardless.) - resume flag — exact form (
--session <id>vs--resume/--continue), and how the resumable session id surfaces in the stream (so we can capture it intoSessionStarted.backendSessionID). - sandbox auth seeding — where
grok auth loginstores credentials under~/.grok, so a containerized run can export them (the Grok analog of Claude's Keychain →.credentials.jsondance inSessionController.containerSpec).
Deliverable of the spike: golden native streams under fixtures/grok/<version>/ (see §8) and
a §6 addendum here that retags each item ✅ with verbatim samples, plus the Tier A/B decision.
3. Architecture mapping
Everything converges on the same AgentBackend → AgentEvent stream →
ApprovalCoordinator.await seam. As shipped (ACP), the Grok adapter is a sibling of
CodexAppServerBackend (both are JSON-RPC stdio backends whose approvals arrive as
server→client requests):
| Concern | Codex app-server (today) | Grok ACP (shipped) |
|---|---|---|
| Invocation | codex app-server |
grok agent stdio |
| Transport | JSONRPCConnection (header omitted) |
JSONRPCConnection (includeVersionHeader: true) |
| Stream decode | CodexAppServerDecoder (thread/turn/item) |
new GrokACPDecoder (session/update variants) → same AgentEvent kinds |
| Approval bridge | …/requestApproval request → ApprovalCoordinator |
session/request_permission request → same ApprovalCoordinator |
| Approval reply | {decision} |
{outcome:{selected,optionId}} (GrokACPDecisionMapping) |
| Resume | thread/resume {threadId} |
session/load {sessionId} (iff agentCapabilities.loadSession) |
| Interrupt | turn/interrupt {threadId,turnId} |
session/cancel {sessionId} |
| Auth | inherited | authenticate {methodId} if authMethods non-empty; else grok login (~/.grok) / XAI_API_KEY |
Historical (original plan): Grok was to be a sibling of
ClaudeCodeBackend—grok -p --output-format streaming-json --permission-mode defaultdecoded by aGrokStreamDecoder, with a generated PreToolUse hook POSTing to the in-process MCP server. ACP superseded this; see the update note at the top.
Reused unchanged: ProcessHost,
ApprovalCoordinator, the HTTP host inside
MCPApprovalServer,
RiskClassifier,
ContainerManager, WorktreeManager, the conflict/lock
system, transcripts, sync, and persistence. The new code is the decoder, the backend actor, the
approval-callback endpoint, and the catalog/UI/factory glue.
4. The pivotal decision — approval tier
Shipped resolution (ACP): neither tier — native permission requests. The hook-vs-fixed-policy tradeoff below was moot once Grok shipped ACP: approvals arrive as native
session/request_permissionserver→client requests, answered with a{outcome:{selected, optionId}}result (GrokACPDecisionMapping) — full interactive approvals (interactiveApprovals: true) with no hook and no HTTP bridge.canModifyToolInputisfalse(ACP's permission outcome can't rewrite tool input). The tier analysis below is historical.
This determines scope. Target the interactive hook bridge (Tier A), with a non-interactive fallback (Tier B) for v1 if the spike forces it.
- Tier A — interactive (target). A generated PreToolUse hook posts each gated tool call to
Nucleic's existing per-session, bearer-token-gated local HTTP server. The handler suspends on
ApprovalCoordinator.await(emittingAgentEvent.approvalRequested), and the hook script emits the allow/deny JSON Grok expects on stdout. This gives Grok sessions the same Mac/iPhone approval UX as Claude →interactiveApprovals: true. Feasible because Grok hooks are explicitly blocking and JSON-in/JSON-out (§1). Paired with--permission-mode default. - Tier B — non-interactive fallback. If the hook contract cannot carry a synchronous human
allow/deny, run Grok like
CodexExecAdapter:ApprovalPolicy.fixedrules +--permission-mode acceptEdits/plan,interactiveApprovals: false, fail-closed for anything unmatched. Lower fidelity but ships, and matches a path the protocol already models. Add aBackendID.grokExecif both tiers end up coexisting.
Everything downstream of ApprovalCoordinator.await is identical either way — the tier only
changes the plumbing that feeds it.
5. Phased implementation plan
Phase 0 — Validation spike (de-risk before the adapter)
Mirror the proven M0 methodology. Install grok, authenticate, run real headless turns against a
throwaway repo. Extend nucleic-spike (or a grok-spike
target) to capture: a simple turn, a tool-approval turn, a multi-file-edit turn, an interrupt, and
a resume. Record golden native streams under fixtures/grok/<version>/ and retag §2 with verbatim
samples. Settle Tier A vs B here.
Phase 1 — Protocol & identifiers (tiny, backward-compatible)
- Add
case grok(andgrokExecif Tier B is needed) toBackendID. Persistence already round-tripsBackendIDby rawValue and falls back to.claudeCodeon unknown (GRDBMetadataStore), so old DBs and the iOS client tolerate the new case for free. AgentEvent.backendand the sync wire (WireMessages) carry it automatically — the phone gets Grok-labeled sessions with no protocol change.
Phase 2 — GrokStreamDecoder
- New
Sources/NucleicCore/Grok/GrokStreamDecoder.swift, structurally likeClaudeStreamDecoderbut decoding xAI's vocab → the existingAgentEvent.Kinds:session.start→sessionStarted,model.message→assistantText,model.thinking→thinking,tool.call→toolCallStarted/toolCallCompleted(+ synthesizefileChangefromwrite_file/edit tools, à lainferredFileChange),tool.result→toolResult,session.end→turnCompleted/runFinished+usage. - Lenient by contract: unknown lines →
.raw(the CLI-drift early warning). Unit-tested against the Phase 0 fixtures (decode → assert exactAgentEvent[]).
Phase 3 — GrokBuildBackend
- New
Sources/NucleicCore/Grok/GrokBuildBackend.swift, an actor mirroringClaudeCodeBackend:static let id = BackendID.grok;capabilitiesset from spike findings (interactiveApprovalsper Tier;nativeResume: true;emitsFileChangeEventsper schema;sandboxModeslikely[]initially).Configurationwithexecutable: "grok", read-only tool pre-allow, single-shot-per-turn stdin handling (the model Claude already uses).- Build args:
-p --output-format streaming-json --permission-mode default(no--modelor--reasoning-effort— Grok Build is one model with Auto reasoning), generated hook/MCP config path, resume via--resume <id>. Reuse the sameProcessHost, stderr-tail, optional raw capture, and exit-synthesis logic. - Approval endpoint (Tier A): add a hook handler to
MCPApprovalServerthat accepts Grok's hook POST and returns its allow/deny JSON, suspending onApprovalCoordinatorexactly likehandleApprovalCall. Generate the.grok/hooksscript + config at run start, pointed athttp://127.0.0.1:<port>with the per-session bearer token (and the VM gateway host for containerized runs, mirroring the Claude path). - Reuse the same
ConflictCoordinatorarbitration in the approval path so Grok sessions participate in the lock system + Nucleic Control autoship just like Claude.
Phase 4 — Auth & env
- Host-spawned (default): Grok auth is inherited naturally — host
~/.groklogin orXAI_API_KEYin the environment. Add a blank-key guard symmetric to the existingANTHROPIC_API_KEYpurge inNucleicAppinit only if the spike shows a blankXAI_API_KEYcauses the same 401-preference problem. - Sandboxed (defer for v1): generalize
SessionController.containerSpecto seed a Grok-home (~/.grok) and forwardXAI_API_KEY/config-dir — the Grok analog of the claude-home seeding. Reasonable to ship Grok host-only first and add container support in a follow-up. Settings already has a per-agent auth model (ControlAuthMode) to extend.
Phase 5 — Backend-aware model catalog
ModelCatalogis Claude-only and global. Refactor to key models/efforts/context-windows byBackendID(or add a parallelGrokModelCatalog): a singlegrok-buildSKU with one "Auto" reasoning level. The home composer's model/effort pickers must switch catalogs based on the selected backend. Grok uses--permission-modemodes rather than Claude's--effort; the genericRunSpec.effort/modelfields stay and each backend translates appropriately.
Phase 6 — Wiring & UI
- Factory dispatch: change the
backendFactoryclosure (and the AppStore call sites) to switch onsession.backend→ClaudeCodeBackendorGrokBuildBackend. Today it ignoressessionand always returns Claude. - Agent picker:
defaultBackendexists onProjectbut has no selector yet. Add (a) a project-settings backend picker, and (b) ideally a per-chat agent picker in the home composer.AppStorealready resolvesproject.defaultBackend ?? .claudeCodeat session creation. - Settings: replace the
Codex "coming soon" placeholder pattern with a real
"Grok" section (default model, permission/auto mode,
XAI_API_KEY/login status), mirroring the Claude section. - Branding: the lavender-accent gating (Nucleic Control only) is orthogonal; Grok sessions render with the standard chrome plus a backend label/icon.
Phase 7 — Testing
fake-grokstub: addSources/fake-grok/analog ofFakeClaudethat replays fixture streams and honors hook callbacks, so backend tests run hermetically with no realgrokbinary (pointConfiguration.executableat it, as Claude tests do).- Decoder unit tests (fixtures →
AgentEvent[]), backend integration tests (run/approve/resume/interrupt), and an approval round-trip test asserting the exact hook-reply bytes Grok accepts. Build/test withswift test --build-system native(the iCloud codesign workaround).
Phase 8 — Quota / polish (optional)
SubscriptionUsage/QuotaIndicatorare Claude-OAuth-specific. Gate the indicator off for Grok sessions until/unless xAI exposes an equivalent usage endpoint.
6. Spike findings (to be filled in by Phase 0)
Placeholder. After the live capture, retag each §2 item ✅ here with verbatim native samples and record the Tier A/B decision, exactly as M0_RESULTS did for Claude.
6.1 Implementation status — adapter landed ahead of the spike (🟡/🔴 pending validation)
Superseded by the ACP migration (2026-06-21). Everything in this subsection describes the original streaming-json + PreToolUse-hook adapter, which has been replaced by the ACP backend (
GrokACPBackend/GrokACPDecoder/GrokACPDecisionMapping; see the update note at the top of this doc and §3/§4). The 🔴-inferred streaming-json/hook unknowns below no longer apply — ACP is a published schema and the wire shapes are pinned to it. Retained as historical context.
The Tier-A adapter is implemented against the inferred schemas so the wiring, decoder, approval bridge, and tests exist and pass hermetically; the §2 unknowns remain the validation backlog and are centralized so a real capture is a local re-pin, not a rewrite:
- Phase 1 — identifiers.
BackendID.grokadded (forModel("grok-*") → .grok); persistence, sync wire, andStatusFeed/xAIfailover route it automatically. - Phase 2 — decoder.
GrokStreamDecodermaps the inferredsession.*/model.*/tool.*vocab →AgentEvents, lenient (.rawon drift). Golden fixtures underfixtures/grok/0.1.0/; unit tests inTests/NucleicCoreTests/GrokStreamDecoderTests.swift. 🔴 the per-event field names are inferred — re-record from a live capture and re-assert. - Phase 3 — backend.
GrokBuildBackend, an actor mirroringClaudeCodeBackend. Tier A approvals: a generated PreToolUse hook (GrokHookConfig) POSTs to the newMCPApprovalServer/grok-hookroute, which suspends on the sameApprovalCoordinator. Joins the conflict-lock system like Claude. Discovery is project-local.grok/(✅ §1) — written into the worktree, git-excluded, restored/removed at teardown — NOT a CLI flag (an earlier--hooks-configguess madegrokexit 2) and NOT a config-dir env (would relocate~/.grokand break auth). 🔴 the hook stdin/stdout contract remains inferred (Claude-Code-compatible); re-pin after the spike. - Phase 4 — auth. Blank-
XAI_API_KEYpurge added toNucleicAppinit (host-only v1). - Phase 5 — catalog & effort. A single
grok-buildSKU ("Grok Build") is offered; the picker doubles as the backend selector. Grok Build is one model with a single "Auto" reasoning mode, so the "Reasoning" menu offers only Auto (plus Orchestra below the divider, an orchestration mode rather than a Grok level). The backend passes neither--modelnor--reasoning-effort— the SKU/effort the UI carries are cosmetic for Grok. (Superseded the earlier plan to drive--reasoning-effort none|low|medium|high, which xAI has since collapsed to Auto.) - Phase 6 — wiring. Factory dispatches
.grok → GrokBuildBackend; Settings names Grok Build. - Phase 7 — tests.
fake-grokstub replays a streaming-json fixture and performs the real/grok-hookround-trip;FakeGrokContractTestscovers allow + deny end-to-end.
Resolved against the real CLI (grok --help, captured 2026-06-21):
- Invocation — headless is
grok -p/--single "<prompt>"(prints to stdout and exits);--output-format streaming-json✅ (plain|json|streaming-json). - Resume —
-r/--resume [<id>]✅ (the--sessionguess was wrong and would have been rejected; fixed). - Permission mode —
--permission-modeacceptsdefault|acceptEdits|auto|dontAsk| bypassPermissions|plan✅; v1 usesdontAsk(headless auto-approve; the PreToolUse hook, which runs first in the gating order, stays the human gate). - Effort — Grok Build is a single model with one "Auto" reasoning mode (xAI picks the
depth itself), so the adapter passes no reasoning flag at all (and no
--model). The "Reasoning" menu offers only Auto; Orchestra is still offered as an orchestration mode. (Earlier captures showed--effort/--reasoning-effortflags; xAI has since collapsed Grok Build to Auto.) - Hook discovery — project-local
.grok/(no flag); verify withgrok inspect.
Still open (a streaming-json capture settles these): the exact session.*/model.*/tool.*
field names and the PreToolUse hook stdin/stdout contract (both decoded/emitted speculatively);
whether dontAsk actually lets the hook gate (the Tier-A assumption); the headless background
wait (--background-wait-timeout defaults to 600s — a turn that leaves a persistent monitor can
appear to hang; --no-wait-for-background is the escape hatch); real model IDs (grok models);
and sandbox auth seeding (deferred — host-only).
7. Effort & risk
- Net-new code is small and localized: one decoder, one backend actor, one approval endpoint, plus catalog/UI/factory glue. The hard infrastructure is reused untouched.
- Backward/forward compatible: the only protocol change is one enum case, which old DBs and the iOS client already tolerate.
- Biggest risk = the PreToolUse hook contract (Tier A). If the spike shows it can't carry a
synchronous human decision, fall back to Tier B (
ApprovalPolicy.fixed, like CodexExec) — still ships, lower fidelity. - Suggested first PR: Phases 0–3 + 6–7, host-only, behind the per-project backend picker. Sandbox (Phase 4 container seeding) and quota (Phase 8) as follow-ups.
8. Fixtures harness
Same shape as ADAPTERS §4:
fixtures/grok/<version>/
01-simple-turn.jsonl + 01-simple-turn.events.json (golden AgentEvent[])
02-tool-approval.jsonl + 02…events.json + hook-call.json / hook-reply.json
03-file-change.jsonl + …
04-resume.jsonl + …
Decoder tests decode the recorded native stream → assert exact AgentEvent[] equality, and (for
approvals) assert the reply bytes the CLI accepts. Re-recording on a CLI upgrade is the
protocol-drift regression signal.