mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 20:46:28 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - AI Connections store reusable provider credentials with company and owner boundaries. > - Self-hosted instances can require board authentication while running agents on the local server. > - The login UI treated those instances as unsupported and displayed instructions without a command. > - Removing that restriction must not expose the server operator's existing CLI account. > - This pull request enables owner-scoped login attempts and reuses the existing login UI. > - Users can connect Codex and Claude subscriptions on an authenticated self-hosted instance. ## Linked Issues or Issue Description **What happened?** On an authenticated self-hosted instance, OpenAI subscription setup displayed terminal instructions with no command and a disabled Connect button. Claude also could not complete the local connection flow. **Expected behavior** An authorized board user can prepare an isolated sign-in attempt, sign in on the server, and save the verified account as a Connection. This must not import another user's or the operator's ambient credentials. **Steps to reproduce** Run Paperclip in authenticated mode with a local environment. Open Connections, choose OpenAI or Anthropic, and select Subscription. The previous UI never enabled local login preparation. Related: #13247, #13248, and #10751. This fix preserves the restriction on remote access to the operator's ambient Claude login. ## What Changed - Allow company-authorized users to create, check, cancel, and complete their own isolated local login attempts. - Keep ambient Claude credential import restricted to the local operator. - Support isolated Claude credential files without falling back to the host account or mutating process-wide environment variables. - Use Codex device authorization so sign-in does not depend on a browser callback to the remote server's localhost. - Gate server-host login on authenticated public deployments unless a trusted runtime host is configured. Publish the capability through health so setup shows supported alternatives. - Read isolated Claude credential files through bounded, descriptor-bound opens with ownership, permission, and symlink checks. Try the alternate filename after malformed JSON. - Reuse shared login instructions and lifecycle hooks in onboarding, agent setup, and Connections. Show health-query failures explicitly. - Document authenticated self-hosted behavior and add authorization, isolation, lifecycle, and UI regression tests. ## Verification - Passed 71 focused tests across connection routes, credential isolation, legacy compatibility, the shared login hook, and agent setup. - Passed 89 onboarding regression tests. - Passed `pnpm -r typecheck`, `pnpm build`, Storybook build, and `pnpm check:token-gates`. - Completed real Codex device authorization and Claude browser authorization on an authenticated Linux self-hosted instance. Both accounts were detected automatically and saved as Connected. Both completed attempt directories were removed. - These live checks cover login, credential validation, and connection creation. They do not establish a new model execution or long-running refresh result. - Review follow-up: 68 focused checks passed after rerunning one route socket error; the full route/health rerun passed all 49 tests. The 70-test onboarding suite also passed. Final workspace typecheck, production build, and Storybook build passed again. - The broad local run exposed an instance-name assumption in two new assertions. The fixture now uses an explicit non-default instance, and all 32 connection tests passed with a different inherited instance name. The superseded broad run was stopped; this is not a claim that the full local suite completed. Full CI results will be recorded before merge. ## Risks - The server must have the provider CLI installed. Users still run the displayed command on the server that hosts Paperclip. - Authorization checks must keep login attempts scoped to the company, owner, provider, and reconnect target. Regression tests cover cross-user and cross-company access. - Existing local-trusted Claude behavior stays available. Authenticated remote users cannot use its ambient import path. - No database migration, dependency change, agent binding change, or provider routing change is included. ## Model Used OpenAI GPT-6 through Codex, with reasoning, code execution, and browser tools. The runtime does not expose a more specific model version or context-window size. ## 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 - [ ] 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>
59 lines
3.2 KiB
TypeScript
59 lines
3.2 KiB
TypeScript
import { readLocalAiCredentialFile } from "./local-ai-credential-file.js";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { readClaudeToken, fetchClaudeQuota } from "@paperclipai/adapter-claude-local/server";
|
|
import { readCodexAuthInfo, fetchCodexQuota } from "@paperclipai/adapter-codex-local/server";
|
|
import { parseGrokAuthPayload, hasUsableGrokAuthValue } from "@paperclipai/adapter-grok-local/server";
|
|
import type { AiProvider } from "@paperclipai/shared";
|
|
import { unprocessable } from "../errors.js";
|
|
|
|
/** Read an owned login home, or an explicitly authorized local-operator import. */
|
|
export async function readVerifiedLocalAiCredential(provider: AiProvider, loginHome?: string): Promise<string> {
|
|
if (provider === "openrouter") throw unprocessable("OpenRouter requires an API key.");
|
|
if ((provider === "openai" || provider === "xai") && !loginHome)
|
|
throw unprocessable("Start a separate local sign-in for this connection before connecting.");
|
|
try {
|
|
if (provider === "anthropic") {
|
|
// Never change process.env or fall back to the server account when an
|
|
// authenticated user's isolated login is missing or invalid.
|
|
let token: string | null = null;
|
|
if (loginHome) {
|
|
for (const name of [".credentials.json", "credentials.json"]) {
|
|
const raw = await readLocalAiCredentialFile(path.join(loginHome, name)).catch(() => null);
|
|
if (!raw) continue;
|
|
let parsed;
|
|
try { parsed = JSON.parse(raw); } catch { continue; }
|
|
const value = parsed?.claudeAiOauth?.accessToken;
|
|
if (typeof value === "string" && value.length) { token = value; break; }
|
|
}
|
|
} else {
|
|
token = await readClaudeToken({ allowKeychain: true });
|
|
}
|
|
if (!token) throw new Error("Missing login");
|
|
await fetchClaudeQuota(token);
|
|
return token;
|
|
}
|
|
if (provider === "openai") {
|
|
const auth = await readCodexAuthInfo(loginHome);
|
|
if (!auth?.accessToken || !auth.refreshToken || !auth.idToken) throw new Error("Missing login");
|
|
await fetchCodexQuota(auth.accessToken, auth.accountId);
|
|
return JSON.stringify({ tokens: { access_token: auth.accessToken, refresh_token: auth.refreshToken, id_token: auth.idToken, account_id: auth.accountId }, last_refresh: auth.lastRefresh });
|
|
}
|
|
const raw = await fs.readFile(path.join(loginHome!, "auth.json"), "utf8");
|
|
const payload = parseGrokAuthPayload(JSON.parse(raw));
|
|
if (!payload || !hasUsableGrokAuthValue(payload.value)) throw new Error("Missing login");
|
|
const response = await fetch("https://api.x.ai/v1/models", {
|
|
headers: { Authorization: `Bearer ${payload.value.key}` },
|
|
redirect: "error", signal: AbortSignal.timeout(15000),
|
|
});
|
|
await response.body?.cancel();
|
|
if (!response.ok) throw new Error("Invalid login");
|
|
return raw;
|
|
} catch {
|
|
// Provider/CLI errors may contain credential material; never return them.
|
|
throw unprocessable(provider === "anthropic" && !loginHome
|
|
? "Could not verify the local subscription. Run claude auth login in a terminal on the machine running Paperclip, then try Connect again."
|
|
: "Could not verify the local subscription. Run the sign-in command shown for this connection, finish signing in, then try Connect again.");
|
|
}
|
|
}
|