Files
nucleic/docs/GROK_ADAPTER.md
T
NucleicandClaude Opus 4.8 609ce70539 Migrate Grok backend to ACP (grok agent stdio)
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]>
2026-06-21 02:52:24 -07:00

24 KiB
Raw Blame History

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-json with a generated PreToolUse hook bridging approvals over HTTP (Tier A, below). That design has been replaced. Grok now ships [agent mode — 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 sets includeVersionHeader: true). No more streaming-json decoder.
  • Approvals: native session/request_permission server→client requeststhe entire /grok-hook HTTP 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/updateAgentEvent), GrokACPDecisionMapping (Decision → ACP permission outcome). Tests: GrokACPDecoderTests, FakeGrokContractTests (a full ACP round-trip against the fake-grok stub); fixtures under fixtures/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-approve in ~/.grok/config.toml for 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, or grok auth login (browser OAuth for SuperGrok); config under ~/.grok (config.toml).
  • Resume: conversations persist; --session latest / --session <id>.
  • Reads existing CLAUDE.md (and AGENTS.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.endnot 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:

  1. 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).
  2. 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).
  3. stdin follow-ups — whether headless grok -p accepts 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.)
  4. resume flag — exact form (--session <id> vs --resume/--continue), and how the resumable session id surfaces in the stream (so we can capture it into SessionStarted.backendSessionID).
  5. sandbox auth seeding — where grok auth login stores credentials under ~/.grok, so a containerized run can export them (the Grok analog of Claude's Keychain → .credentials.json dance in SessionController.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 AgentBackendAgentEvent 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 ClaudeCodeBackendgrok -p --output-format streaming-json --permission-mode default decoded by a GrokStreamDecoder, 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_permission server→client requests, answered with a {outcome:{selected, optionId}} result (GrokACPDecisionMapping) — full interactive approvals (interactiveApprovals: true) with no hook and no HTTP bridge. canModifyToolInput is false (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 (emitting AgentEvent.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.fixed rules + --permission-mode acceptEdits/plan, interactiveApprovals: false, fail-closed for anything unmatched. Lower fidelity but ships, and matches a path the protocol already models. Add a BackendID.grokExec if 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 (and grokExec if Tier B is needed) to BackendID. Persistence already round-trips BackendID by rawValue and falls back to .claudeCode on unknown (GRDBMetadataStore), so old DBs and the iOS client tolerate the new case for free.
  • AgentEvent.backend and 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 like ClaudeStreamDecoder but decoding xAI's vocab → the existing AgentEvent.Kinds: session.startsessionStarted, model.messageassistantText, model.thinkingthinking, tool.calltoolCallStarted/toolCallCompleted (+ synthesize fileChange from write_file/edit tools, à la inferredFileChange), tool.resulttoolResult, session.endturnCompleted/runFinished + usage.
  • Lenient by contract: unknown lines → .raw (the CLI-drift early warning). Unit-tested against the Phase 0 fixtures (decode → assert exact AgentEvent[]).

Phase 3 — GrokBuildBackend

  • New Sources/NucleicCore/Grok/GrokBuildBackend.swift, an actor mirroring ClaudeCodeBackend:
    • static let id = BackendID.grok; capabilities set from spike findings (interactiveApprovals per Tier; nativeResume: true; emitsFileChangeEvents per schema; sandboxModes likely [] initially).
    • Configuration with executable: "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 --model or --reasoning-effort — Grok Build is one model with Auto reasoning), generated hook/MCP config path, resume via --resume <id>. Reuse the same ProcessHost, stderr-tail, optional raw capture, and exit-synthesis logic.
    • Approval endpoint (Tier A): add a hook handler to MCPApprovalServer that accepts Grok's hook POST and returns its allow/deny JSON, suspending on ApprovalCoordinator exactly like handleApprovalCall. Generate the .grok/hooks script + config at run start, pointed at http://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 ConflictCoordinator arbitration 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 ~/.grok login or XAI_API_KEY in the environment. Add a blank-key guard symmetric to the existing ANTHROPIC_API_KEY purge in NucleicApp init only if the spike shows a blank XAI_API_KEY causes the same 401-preference problem.
  • Sandboxed (defer for v1): generalize SessionController.containerSpec to seed a Grok-home (~/.grok) and forward XAI_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

  • ModelCatalog is Claude-only and global. Refactor to key models/efforts/context-windows by BackendID (or add a parallel GrokModelCatalog): a single grok-build SKU with one "Auto" reasoning level. The home composer's model/effort pickers must switch catalogs based on the selected backend. Grok uses --permission-mode modes rather than Claude's --effort; the generic RunSpec.effort/model fields stay and each backend translates appropriately.

Phase 6 — Wiring & UI

  • Factory dispatch: change the backendFactory closure (and the AppStore call sites) to switch on session.backendClaudeCodeBackend or GrokBuildBackend. Today it ignores session and always returns Claude.
  • Agent picker: defaultBackend exists on Project but has no selector yet. Add (a) a project-settings backend picker, and (b) ideally a per-chat agent picker in the home composer. AppStore already resolves project.defaultBackend ?? .claudeCode at 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-grok stub: add Sources/fake-grok/ analog of FakeClaude that replays fixture streams and honors hook callbacks, so backend tests run hermetically with no real grok binary (point Configuration.executable at 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 with swift test --build-system native (the iCloud codesign workaround).

Phase 8 — Quota / polish (optional)

  • SubscriptionUsage / QuotaIndicator are 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.grok added (forModel("grok-*") → .grok); persistence, sync wire, and StatusFeed/xAI failover route it automatically.
  • Phase 2 — decoder. GrokStreamDecoder maps the inferred session.*/model.*/tool.* vocab → AgentEvents, lenient (.raw on drift). Golden fixtures under fixtures/grok/0.1.0/; unit tests in Tests/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 mirroring ClaudeCodeBackend. Tier A approvals: a generated PreToolUse hook (GrokHookConfig) POSTs to the new MCPApprovalServer /grok-hook route, which suspends on the same ApprovalCoordinator. 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-config guess made grok exit 2) and NOT a config-dir env (would relocate ~/.grok and break auth). 🔴 the hook stdin/stdout contract remains inferred (Claude-Code-compatible); re-pin after the spike.
  • Phase 4 — auth. Blank-XAI_API_KEY purge added to NucleicApp init (host-only v1).
  • Phase 5 — catalog & effort. A single grok-build SKU ("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 --model nor --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-grok stub replays a streaming-json fixture and performs the real /grok-hook round-trip; FakeGrokContractTests covers 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 --session guess was wrong and would have been rejected; fixed).
  • Permission mode--permission-mode accepts default|acceptEdits|auto|dontAsk| bypassPermissions|plan ; v1 uses dontAsk (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-effort flags; xAI has since collapsed Grok Build to Auto.)
  • Hook discovery — project-local .grok/ (no flag); verify with grok 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 03 + 67, 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.