Files
PaperClipAI/server/src/routes/adapters.test.ts
T
Nicky LeachandPaperclip a20a4944ec feat: add Grok device login to the sandbox login panel (#12469)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip uses adapters to connect agents and model providers to its
control plane
> - The sandbox login panel supports displayed-code login for selected
adapters
> - Grok users need the same login path and a private credential home
for later runs
> - This pull request adds Grok support to the shared device-login path
and preserves the existing Codex path
> - The benefit is one secure login flow for both adapters with
company-scoped credential storage

## Linked Issues or Issue Description

**Agent or provider**

Grok Local needs displayed-code login support in the sandbox login
panel.

**Why this adapter is useful**

This change lets users sign in to Grok from the sandbox login panel. It
also gives later Grok runs access to the stored credential.

**How the agent is invoked**

The Grok local adapter uses its login command through the shared
displayed-code login flow. Later runs receive the managed home through
`GROK_HOME`.

**Additional context**

The change uses adapter-scoped login lifecycle handling. It stores the
credential in a company-scoped directory with mode `0700`, and it stores
the credential file with mode `0600`.

## What Changed

- Rename the shared device-login modules to adapter-neutral names.
- Scope the shared login lifecycle to a closed adapter set.
- Return the device-login URL that the provider prints.
- Add the Grok prompt parser, login command, capability, and login panel
entry.
- Store the Grok credential in a private, company-scoped home directory.
- Pass `GROK_HOME` to later Grok runs.
- Add tests for the Grok adapter, the Daytona sandbox provider, the
server login path, and the user interface.

## Verification

- Run `pnpm vitest run
packages/adapters/grok-local/src/server/adapter-auth-promotion.test.ts`.
- Run the Grok adapter package suite.
- Run the Daytona sandbox provider suite.
- Run the server device-login suites.
- Run the user interface suite.
- Confirm the full CI suite passes.

## Risks

The change extends shared login lifecycle code to another adapter. A
regression could affect Codex login. The credential path uses explicit
`chmod` calls to keep the directory at mode `0700` and the file at mode
`0600`.

## Model Used

OpenAI Codex, GPT-5. The runtime used tool calls and code review
support. The runtime did not provide a context-window value.

## 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
- [ ] 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>
2026-08-28 21:48:34 -07:00

78 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AdapterLoginCapability, ServerAdapterModule } from "@paperclipai/adapter-utils";
import { requireServerAdapter } from "../adapters/registry.js";
import { buildAdapterCapabilities } from "./adapters.js";
// The adapter listing projects the safe scalar login fields to the client. The
// projection carries the panel mode, the sandbox transport, and the timeout
// policy. It never carries a function member or a secret. An adapter with no
// login capability projects no `login` object.
function makeAdapter(overrides: Partial<ServerAdapterModule> = {}): ServerAdapterModule {
return {
type: "vendor_local",
execute: async () => {
throw new Error("not used");
},
testEnvironment: async () => {
throw new Error("not used");
},
...overrides,
} as ServerAdapterModule;
}
const displayedCodeLogin: AdapterLoginCapability = {
panelMode: "displayed_code",
timeoutPolicy: "caller_bounded",
getCommand: () => "vendor login",
parsePrompt: () => null,
};
describe("buildAdapterCapabilities login projection", () => {
it("projects the safe scalar login fields", () => {
const caps = buildAdapterCapabilities(makeAdapter({ loginCapability: displayedCodeLogin }));
expect(caps.login).toEqual({
panelMode: "displayed_code",
timeoutPolicy: "caller_bounded",
});
});
it("omits the login object when the adapter declares no capability", () => {
const caps = buildAdapterCapabilities(makeAdapter());
expect(caps.login).toBeUndefined();
});
it("never projects the function members or a completion claim", () => {
const caps = buildAdapterCapabilities(
makeAdapter({
loginCapability: {
panelMode: "submitted_browser_code",
timeoutPolicy: "fixed",
getCommand: () => "vendor setup-token",
parsePrompt: () => null,
captureCredential: () => null,
completionClaim: "storedSessionId",
},
}),
);
expect(caps.login).toEqual({
panelMode: "submitted_browser_code",
timeoutPolicy: "fixed",
});
expect(caps.login).not.toHaveProperty("getCommand");
expect(caps.login).not.toHaveProperty("parsePrompt");
expect(caps.login).not.toHaveProperty("captureCredential");
expect(caps.login).not.toHaveProperty("completionClaim");
});
it("projects panelMode and timeoutPolicy for the registered grok_local adapter, with no function member", () => {
const caps = buildAdapterCapabilities(requireServerAdapter("grok_local"));
expect(caps.login).toEqual({
panelMode: "displayed_code",
timeoutPolicy: "caller_bounded",
});
expect(caps.login).not.toHaveProperty("getCommand");
expect(caps.login).not.toHaveProperty("parsePrompt");
});
});