Skip to content

feat(grok): add Grok CLI provider via ACP#2809

Open
Jaaneek wants to merge 11 commits into
pingdotgg:mainfrom
Jaaneek:feat/grok-provider
Open

feat(grok): add Grok CLI provider via ACP#2809
Jaaneek wants to merge 11 commits into
pingdotgg:mainfrom
Jaaneek:feat/grok-provider

Conversation

@Jaaneek
Copy link
Copy Markdown

@Jaaneek Jaaneek commented May 26, 2026

What Changed

#2808
Add Grok Build via ACL

Why

Grok Build was recently released and its accessible via X Premium Subs and Supergrok Subs

UI Changes

image

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Medium Risk
New provider stack touches orchestration turn routing and external CLI/ACP processes; model-change gating is a behavioral change for restricted providers but is covered by server and client tests.

Overview
Adds Grok as a first-class provider: a new GrokDriver runs the Grok CLI over ACP (grok agent stdio), with adapter sessions, permissions, plan/tool streaming, session/set_model, and the _x.ai/ask_user_question extension mapped to user-input flows. Provider health checks probe grok --version and discover models via a short ACP startup; snapshots advertise requiresNewThreadForModelChange.

Orchestration and UI now honor that flag: after a thread has started, ProviderCommandReactor rejects turn starts that change model/instance when either side requires a new thread, and the composer shows a warning toast via shared logic in ChatView.

Headless text generation for Grok uses the same ACP path (GrokTextGeneration) for commits, PRs, branch names, and titles. Contracts/settings gain GrokSettings, default model grok-build, and ServerProvider.requiresNewThreadForModelChange; the web app registers icons, picker entries, and settings forms. Tests and the ACP mock agent are extended for Grok models, custom approval option IDs, and xAI ask-user flows.

Reviewed by Cursor Bugbot for commit 2177da0. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Grok CLI provider via ACP with text generation and session model switching

  • Introduces a full grok provider driver (GrokDriver.ts) that spawns the Grok CLI via ACP, discovers models, checks provider status, and surfaces snapshot enrichment with version advisories.
  • Adds GrokAdapter for session lifecycle management including approval/user-input flows, xAI extension handling (_x.ai/ask_user_question), and native event logging.
  • Implements GrokTextGeneration for commit messages, PR content, branch names, and thread titles via ACP prompts with a 180s timeout and structured JSON output.
  • Adds a requiresNewThreadForModelChange flag to ServerProvider snapshots; ProviderCommandReactor now rejects mid-thread model changes when this flag is set on either the current or target provider.
  • Registers grok in contracts, settings, built-in drivers, provider icon map, and the UI provider picker with an Early Access/new badge.
  • Risk: Grok sessions require starting a new thread to switch models — attempts to switch mid-conversation are rejected server-side and blocked client-side with a warning toast.

Macroscope summarized 2177da0.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6c57ab2d-3399-4e06-af25-a6ec728e53ad

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 26, 2026
Comment thread apps/server/src/provider/Layers/GrokAdapter.ts
Comment thread apps/server/src/provider/Layers/GrokAdapter.ts
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 26, 2026

Approvability

Verdict: Needs human review

3 blocking correctness issues found. This PR introduces a complete new Grok provider integration with substantial new code (~3400 lines). As a new feature adding new workflows and integrations, it warrants human review. Additionally, there are 4 unresolved medium-severity review comments identifying potential race conditions and logic issues that should be addressed.

You can customize Macroscope's approvability policy. Learn more.

Wires the Grok CLI (https://x.ai/cli) into the existing ACP runtime so
X Premium / SuperGrok users get a first-class provider.

- GrokDriver registered through builtInDrivers
- GrokProvider / GrokAdapter handle session lifecycle, event streams,
  and model switching via session/set_model
- GrokAcpSupport probes auth methods and drives the xai.api_key flow
- GrokTextGeneration adds Grok as an option (not a default) for git
  commit/branch generation
- grok-build registered as the only Grok model id (only one currently
  exposed by the CLI)
- UI: Grok icon, provider selector entry, settings driver metadata,
  provider icon mapping

Defaults are unchanged: Codex remains the default provider.

Closes pingdotgg#2808
@Jaaneek Jaaneek force-pushed the feat/grok-provider branch from 30e7372 to a58e850 Compare May 26, 2026 05:33
@anxkhn
Copy link
Copy Markdown

anxkhn commented May 28, 2026

was looking for this, would really appreciate if it's tested well and merged to main soon.

- Use crypto-backed UUIDs and normalize ACP callback failures
- Fix chat composer refs and browser secret record updates
- Tighten runtime catalog hydration test setup
- Use provider-supplied approval option IDs when responding
- Keep streaming if native notification logging fails
Comment on lines +732 to +743
const interruptTurn: GrokAdapterShape["interruptTurn"] = (threadId) =>
Effect.gen(function* () {
const ctx = yield* requireSession(threadId);
yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals);
yield* Effect.ignore(
ctx.acp.cancel.pipe(
Effect.mapError((error) =>
mapAcpToAdapterError(PROVIDER, threadId, "session/cancel", error),
),
),
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/GrokAdapter.ts:732

interruptTurn modifies session state without acquiring the thread lock, so it can race with sendTurn or stopSession on the same threadId. This allows concurrent mutation of ctx.pendingApprovals and overlapping calls to ctx.acp.cancel while another operation is mid-execution. Consider wrapping the body in withThreadLock(threadId, ...) to match the other session-modifying operations.

-    const interruptTurn: GrokAdapterShape["interruptTurn"] = (threadId) =>
-      Effect.gen(function* () {
-        const ctx = yield* requireSession(threadId);
-        yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals);
-        yield* Effect.ignore(
-          ctx.acp.cancel.pipe(
-            Effect.mapError((error) =>
-              mapAcpToAdapterError(PROVIDER, threadId, "session/cancel", error),
-            ),
-          ),
-        );
-      });
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/provider/Layers/GrokAdapter.ts around lines 732-743:

`interruptTurn` modifies session state without acquiring the thread lock, so it can race with `sendTurn` or `stopSession` on the same `threadId`. This allows concurrent mutation of `ctx.pendingApprovals` and overlapping calls to `ctx.acp.cancel` while another operation is mid-execution. Consider wrapping the body in `withThreadLock(threadId, ...)` to match the other session-modifying operations.

Evidence trail:
GrokAdapter.ts line 209-210: `withThreadLock` definition uses per-thread Semaphore with permit=1. Line 305: `startSession` uses `withThreadLock`. Lines 581, 688: `sendTurn` uses `withThreadLock`. Line 797: `stopSession` uses `withThreadLock`. Lines 732-743: `interruptTurn` does NOT use `withThreadLock` but mutates `ctx.pendingApprovals` via `settlePendingApprovalsAsCancelled` and calls `ctx.acp.cancel`. Lines 285-302: `stopSessionInternal` (called from `stopSession` under lock) also calls `settlePendingApprovalsAsCancelled(ctx.pendingApprovals)`.

@gmackie
Copy link
Copy Markdown

gmackie commented Jun 3, 2026

Thanks for pushing this forward. I think the concurrency comments are valid and not really Grok-specific; they belong at the shared ACP adapter boundary.

A safe shape is:

  1. prevent overlapping sendTurn calls for the same ACP session/thread, without holding the same lock across session/prompt in a way that would deadlock approval/cancel flows;
  2. ensure every emitted turn.started gets a terminal event even when session/prompt fails;
  3. coordinate interruptTurn with the same per-thread session state used by send/stop.

I opened #2932 with that shape for review. It extracts Cursor/Grok into a shared makeAcpProviderAdapter(...), adds Grok Build as a thin wrapper using grok --agent build agent stdio, and includes regressions for prompt failure after turn.started plus overlapping turns. The intent is that these lifecycle concerns are fixed once for ACP providers rather than copied into each provider adapter.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b6b8e4a. Configure here.

),
),
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interrupt lacks thread lock

Medium Severity

interruptTurn does not use withThreadLock while sendTurn can be mid-session/prompt outside the lock. Cancel and a new turn can race on the same session without coordinated serialization.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b6b8e4a. Configure here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also like that in other providers I think?

Comment on lines +122 to +136
export function extractXAiAskUserQuestions(params: unknown): ReadonlyArray<UserInputQuestion> {
const root = unwrapParams(params);
const title = stringField(root, ["title", "header", "toolTitle"]);
const questions = arrayField(root, ["questions", "items", "prompts"]);
if (questions.length > 0) {
return questions.map((question, index) => extractQuestion(question, title, index));
}
const singleQuestion = nestedRecord(root, ["question"]) ?? root;
const singleQuestionOptions = arrayField(root, ["options", "choices", "answers"]);
const question =
singleQuestion === root || singleQuestionOptions.length === 0
? singleQuestion
: { ...singleQuestion, options: singleQuestionOptions };
return [extractQuestion(question, title, 0)];
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium acp/XAiAcpExtension.ts:122

In the multi-question code path, when a question has a nested question object with sibling options at the top level, the options are ignored. For example, { question: { prompt: "Pick one" }, options: ["A", "B"] } returns the default [{ label: "OK", description: "Continue" }] instead of the provided options. The questionSource variable points to the nested object, but extractOptions is called on questionSource instead of record, so sibling options are lost. The single-question path in extractXAiAskUserQuestions handles this by merging options before calling extractQuestion, but the multi-question path lacks this step.

export function extractXAiAskUserQuestions(params: unknown): ReadonlyArray<UserInputQuestion> {
   const root = unwrapParams(params);
   const title = stringField(root, ["title", "header", "toolTitle"]);
   const questions = arrayField(root, ["questions", "items", "prompts"]);
   if (questions.length > 0) {
-    return questions.map((question, index) => extractQuestion(question, title, index));
+    return questions.map((question, index) => {
+      const record = isRecord(question) ? question : {};
+      const nestedQuestion = nestedRecord(record, ["question"]);
+      const options = arrayField(record, ["options", "choices", "answers"]);
+      const questionWithOptions =
+        nestedQuestion && options.length > 0
+          ? { ...nestedQuestion, options }
+          : question;
+      return extractQuestion(questionWithOptions, title, index);
+    });
   }
   const singleQuestion = nestedRecord(root, ["question"]) ?? root;
   const singleQuestionOptions = arrayField(root, ["options", "choices", "answers"]);
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/provider/acp/XAiAcpExtension.ts around lines 122-136:

In the multi-question code path, when a question has a nested `question` object with sibling `options` at the top level, the options are ignored. For example, `{ question: { prompt: "Pick one" }, options: ["A", "B"] }` returns the default `[{ label: "OK", description: "Continue" }]` instead of the provided options. The `questionSource` variable points to the nested object, but `extractOptions` is called on `questionSource` instead of `record`, so sibling `options` are lost. The single-question path in `extractXAiAskUserQuestions` handles this by merging options before calling `extractQuestion`, but the multi-question path lacks this step.

Evidence trail:
File: apps/server/src/provider/acp/XAiAcpExtension.ts at REVIEWED_COMMIT
- Lines 97-120: `extractQuestion` function — line 104 sets `questionSource = nestedQuestion ?? record`, line 118 calls `extractOptions(arrayField(questionSource, ...))` which searches `questionSource` (the nested object) not `record` (the original element)
- Lines 80-95: `extractOptions` returns default `[{ label: "OK", description: "Continue" }]` when given empty array (line 94)
- Lines 126-128: multi-question path passes each question directly to `extractQuestion` without merging sibling options
- Lines 129-135: single-question path explicitly merges sibling options via `{ ...singleQuestion, options: singleQuestionOptions }` at line 134

Comment on lines +551 to +618
const nf = yield* Stream.runDrain(
Stream.mapEffect(acp.getEvents(), (event) =>
Effect.gen(function* () {
switch (event._tag) {
case "AssistantItemStarted":
yield* offerRuntimeEvent(
makeAcpAssistantItemEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: ctx.activeTurnId,
itemId: event.itemId,
lifecycle: "item.started",
}),
);
return;
case "AssistantItemCompleted":
yield* offerRuntimeEvent(
makeAcpAssistantItemEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: ctx.activeTurnId,
itemId: event.itemId,
lifecycle: "item.completed",
}),
);
return;
case "PlanUpdated":
yield* logNative(ctx.threadId, "session/update", event.rawPayload);
yield* emitPlanUpdate(ctx, event.payload, event.rawPayload, "session/update");
return;
case "ToolCallUpdated":
yield* logNative(ctx.threadId, "session/update", event.rawPayload);
yield* offerRuntimeEvent(
makeAcpToolCallEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: ctx.activeTurnId,
toolCall: event.toolCall,
rawPayload: event.rawPayload,
}),
);
return;
case "ContentDelta":
yield* logNative(ctx.threadId, "session/update", event.rawPayload);
yield* offerRuntimeEvent(
makeAcpContentDeltaEvent({
stamp: yield* makeEventStamp(),
provider: PROVIDER,
threadId: ctx.threadId,
turnId: ctx.activeTurnId,
...(event.itemId ? { itemId: event.itemId } : {}),
text: event.text,
rawPayload: event.rawPayload,
}),
);
return;
}
}),
),
).pipe(
Effect.catch((cause) =>
Effect.logError("Failed to process Grok runtime notification.", { cause }),
),
Effect.forkChild,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/GrokAdapter.ts:551

The catch at line 613-616 wraps Stream.runDrain, so any single event failure terminates the entire notification fiber. Once terminated, the session stops receiving all runtime events while still appearing active. Consider moving error handling inside the Stream.mapEffect callback to catch per-event failures without stopping the stream.

-      ).pipe(
-        Effect.catch((cause) =>
-          Effect.logError("Failed to process Grok runtime notification.", { cause }),
-        ),
-        Effect.forkChild,
-      );
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/provider/Layers/GrokAdapter.ts around lines 551-618:

The `catch` at line 613-616 wraps `Stream.runDrain`, so any single event failure terminates the entire notification fiber. Once terminated, the session stops receiving all runtime events while still appearing active. Consider moving error handling inside the `Stream.mapEffect` callback to catch per-event failures without stopping the stream.

Evidence trail:
apps/server/src/provider/Layers/GrokAdapter.ts lines 551-620 (REVIEWED_COMMIT): Stream.runDrain wraps the entire stream; Effect.catch at line 613-616 catches after drain terminates; Effect.forkChild at line 617 forks the recovered effect.
apps/server/src/provider/Layers/GrokAdapter.ts line 99 (REVIEWED_COMMIT): notificationFiber typed as Fiber.Fiber<void, never> — never error type confirms catch fully recovers.
apps/server/src/provider/Layers/GrokAdapter.ts lines 306-316 (REVIEWED_COMMIT): stopSessionInternal is the only path that calls sessions.delete and emits session.exited — not triggered by fiber completion.
apps/server/src/provider/Layers/GrokAdapter.ts line 620: ctx.notificationFiber = nf — session retains reference but has no mechanism to detect fiber completion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants