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

+3 -1
View File
@@ -2,7 +2,9 @@
## Current implementation (2026-09-07)
The shipped new-agent flow starts from **Agents → New Agent**. A small dialog collects a name and an enabled adapter, then opens a setup page with numbered navigation. Claude and Codex have a subscription/API-key connection step. Configuration provides a searchable, free-text model selector and an environment selector; additional settings remain on the full agent page. **Finish setup** submits the existing governed hire request. Confirmation links to configuration and opens a new task with the created agent assigned; agents awaiting approval cannot be assigned work yet.
The shipped new-agent flow starts from **Agents → New Agent**. A small dialog collects a name and an enabled adapter, then opens a setup page with numbered navigation. Claude, Codex, and Grok have a subscription/API-key connection step. Configuration provides a searchable, free-text model selector and an environment selector; additional settings remain on the full agent page. **Finish setup** submits the existing governed hire request. Confirmation links to configuration and opens a new task with the created agent assigned; agents awaiting approval cannot be assigned work yet.
On Cloud, the new-agent picker and direct setup links allow Claude, Codex, OpenCode, and Grok. Four available adapters use a two-column grid on desktop and mobile. Grok uses the existing `grok_local` adapter with an xAI subscription or API-key connection and the managed sandbox environment. The picker and connection step share provider marks that adapt to light and dark mode. An adapter must also be loaded and enabled to be available.
Paperclip Runner offers native Codex (app server), Claude via ACPX, and OpenCode. The selected provider is passed through the existing runner configuration builder. Codex's default is the adapter catalog default. OpenCode and Pi require a provider/model ID; OpenRouter uses `openrouter/<provider>/<model>` and `OPENROUTER_API_KEY`. Entered keys are tested through the probe-only `testCredentials` field without storage. Finishing setup saves each key as an isolated user secret so a failed test cannot overwrite another agent’s credential, and an existing organization secret can also be bound. Agent configuration and revisions contain references, never the entered key.
+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();