feat(ui): enable Grok in Cloud agent setup (#13791)

## Thinking Path

> - Paperclip manages AI agents and their execution settings.
> - The new-agent flow selects an adapter before configuring credentials
and a model.
> - Cloud uses one adapter policy for the picker and direct setup links.
> - That policy excludes Grok despite its existing adapter and xAI
connection support.
> - This change adds Grok to the Cloud policy.
> - Cloud users can configure Grok with the existing subscription or
API-key flow.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Agent creation on Cloud.

**Current behavior**

The Cloud picker offers Claude, Codex, and OpenCode. Direct Grok setup
links also fail the shared adapter check.

**Proposed behavior**

Offer Grok alongside the existing choices. Use the existing xAI
connection and managed sandbox setup.

**Reason and benefit**

Users can select an already-supported adapter through the Cloud creation
flow.

**Breaking changes**

None. Loaded and enabled checks still apply. Other excluded adapters
remain excluded.

## What Changed

- Add `grok_local` to the shared Cloud creation policy.
- Display four adapter choices in a 2×2 grid on desktop and mobile, and
reuse the theme-aware provider mark on the connection step so Grok is
visible in dark mode.
- Verify picker navigation and both Grok authentication methods through
sandbox setup, model testing, and agent creation.
- Verify that probe and hire payloads carry the xAI connection binding
without the entered API key.
- Update the agent configuration specification.

## Verification

- `cd ui && pnpm exec vitest run src/components/NewAgentDialog.test.tsx
src/pages/NewAgent.test.tsx
src/components/new-agent/AgentProviderConnection.test.tsx` — 69 tests
passed.
- Chromium checks against the production components and built stylesheet
— four cards occupy two rows and two columns at 1280px and 390px; the
connection step loads and displays the white Grok logo in dark mode and
the black logo in light mode.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm --filter @paperclipai/ui build` — passed.
- `pnpm check:token-gates` — passed.
- `git diff origin/master...HEAD | gitleaks stdin --redact --no-banner`
— no leaks found; manual diff review found no private identifiers or
user data.
- `pnpm -r typecheck` and `pnpm build` — blocked at the existing runner
package because `cargo` is not installed locally.
- `pnpm test:run` — started locally, then stopped after the equivalent
CI suites passed.
- Initial [PR
CI](https://github.com/paperclipai/paperclip/actions/runs/35681351752)
passed, including full typecheck/build, general and serialized tests,
Rust checks, and all eight browser-test shards. One unchanged Cursor
test timed out on the first attempt; its five-test file passed locally
and the failed CI shard passed on retry.
- The [latest CI
run](https://github.com/paperclipai/paperclip/actions/runs/35683642812)
passed build, typecheck, all general and serialized tests, Rust checks,
and all eight browser-test shards. One unchanged
local-service-supervisor test failed its HTTP readiness check on the
first attempt; its six-test file passed locally, and the failed server
shard passed on retry.
- Live xAI login and model execution were not run; the setup tests mock
provider calls.

## Risks

Small UI policy change. The existing Grok adapter, authentication, and
secret storage paths remain in use. No schema or control-plane change is
required. Cloud must deploy a tenant-app release containing this change.
Revert the policy entry to hide Grok from new-agent setup again.

## Model Used

- OpenAI GPT-6 (Codex), with repository inspection, code editing, and
shell-based verification.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-09-22 09:17:59 -07:00
1 parent 8725d6ce09
commit 83abfa46f6
6 files changed
+68 -13

No files matched your search

+9 -2
View File
@@ -132,7 +132,7 @@ it.each([false, undefined])(
},
);
it("offers only Claude, Codex, and OpenCode on Cloud, even with the runner enabled", async () => {
it("offers Claude, Codex, OpenCode, and Grok on Cloud, even with the runner enabled", async () => {
await act(async () => {
cache.setQueryData(queryKeys.health, {
status: "ok",
@@ -160,8 +160,15 @@ it("offers only Claude, Codex, and OpenCode on Cloud, even with the runner enabl
[...document.querySelectorAll<HTMLInputElement>('input[type="radio"]')].map(
(input) => input.value,
),
).toEqual(["claude_local", "codex_local", "opencode_local"]);
).toEqual(["claude_local", "codex_local", "opencode_local", "grok_local"]);
expect(document.body.textContent).not.toContain("CLI harness");
await act(async () =>
document.querySelector<HTMLInputElement>('input[value="grok_local"]')!.click(),
);
await click("Configure agent");
const query = new URL(state.navigate.mock.calls[0][0], "http://local").searchParams;
expect(query.get("adapterType")).toBe("grok_local");
expect(query.get("name")).toBe("Ada & Co");
});
it("keeps agent-only invitations reachable from the new-agent flow", async () => {
@@ -226,7 +226,7 @@ export function AgentBasicsDialog({
{error.message}
</p>
)}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
<div className={cn("grid grid-cols-2 gap-3", choices.length !== 4 && "sm:grid-cols-3")}>
{choices.map((adapter) => {
const display = getAdapterDisplay(adapter.type);
return (
@@ -12,6 +12,7 @@ import {
import { agentsApi } from "@/api/agents";
import { queryKeys } from "@/lib/queryKeys";
import { AdapterLoginPanel } from "../AgentConfigForm";
import { AdapterMark } from "./AgentBasicsDialog";
import {
LocalProviderLoginInstructions,
OnboardingCardField,
@@ -222,13 +223,7 @@ export function AgentProviderConnection({
{
id: adapterType,
label: provider,
icon: (
<img
src={adapterType === "grok_local" ? "/brands/adapters/grok.svg" : `/brands/${adapterType === "claude_local" ? "claude" : "codex"}-color.svg`}
className="size-6"
alt=""
/>
),
icon: <AdapterMark type={adapterType} />,
},
]}
mode={method}
+1
View File
@@ -2,6 +2,7 @@ const CLOUD_ADAPTERS = new Set([
"claude_local",
"codex_local",
"opencode_local",
"grok_local",
]);
/** Creation policy shared by the picker and direct setup links. */
+52 -2
View File
@@ -80,8 +80,8 @@ vi.mock("@/components/AgentConfigForm", () => ({
onChange={(e) => onChange(e.target.value)}
/>
),
AdapterLoginPanel: ({ onStored }: { onStored: (id: string) => void }) => (
<button onClick={() => onStored("stored-claim")}>
AdapterLoginPanel: ({ onConnected }: { onConnected: (id: string) => void }) => (
<button onClick={() => onConnected("login-session")}>
Complete subscription login
</button>
),
@@ -213,6 +213,56 @@ describe("New agent setup", () => {
expect(container.textContent).toContain("This adapter is unavailable");
expect(api.hire).not.toHaveBeenCalled();
});
it.each(["subscription", "api_key"])("configures Grok on Cloud with an xAI %s connection", async (method) => {
cache.setQueryData(queryKeys.health, {
status: "ok",
cloud: { managed: true },
});
envApi.list.mockResolvedValue([
{ id: "sandbox-1", name: "Paperclip Cloud", driver: "sandbox", config: { provider: "daytona" } },
]);
envApi.capabilities.mockResolvedValue({
sandboxProviders: { daytona: { supportsLoginPty: true } },
});
settings.get.mockResolvedValue({ defaultEnvironmentId: "sandbox-1" });
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
api.getAdapterAuthSignal.mockResolvedValue({ status: "missing" });
api.testEnvironment.mockResolvedValue({ ...pass, adapterType: "grok_local" });
await render("grok_local");
expect(container.textContent).toContain("Connect Atlas to Grok");
if (method === "subscription") {
await click("GrokSubscription");
await click("Complete subscription login");
} else {
await click("Use API key insteadUse subscription insteadUse API key instead");
await click("GrokAPI");
await fill("API key", "example-test-secret");
await click("Connect");
expect(managedApi.create).toHaveBeenCalledWith("company-1", expect.objectContaining({
provider: "xai", method: "api_key", apiKey: "example-test-secret",
}));
}
await fill("Model", "grok-code-fast-1");
await click("Run test");
const binding = { provider: "xai", method, mode: "responsible_user" };
expect(api.testEnvironment).toHaveBeenLastCalledWith("company-1", "grok_local", expect.objectContaining({
environmentId: "sandbox-1",
adapterConfig: expect.objectContaining({ model: "grok-code-fast-1" }),
aiConnection: binding,
testCredentials: {},
}));
await click("Finish setup");
expect(api.hire).toHaveBeenCalledTimes(1);
expect(api.hire.mock.calls[0][1]).toMatchObject({
adapterType: "grok_local",
defaultEnvironmentId: "sandbox-1",
adapterConfig: { model: "grok-code-fast-1" },
runtimeConfig: { aiConnection: binding, heartbeat: { enabled: false } },
});
expect(JSON.stringify(api.testEnvironment.mock.calls)).not.toContain("example-test-secret");
expect(JSON.stringify(api.hire.mock.calls)).not.toContain("example-test-secret");
expect(container.textContent).toContain("Your agent is ready");
});
it("sends Cursor Cloud repo/ref and transient API key, then saves an organization secret", async () => {
await render("cursor_cloud");
expect(container.querySelector('[aria-label="Model"]')).toBeNull();