mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Users connect provider accounts during onboarding and agent setup. > - They should reuse and manage those accounts through the existing Connectors interface. > - A second login wizard would diverge from the established provider workflows. > - This pull request composes the existing sign-in components into Connections and agent configuration. > - Users can select accounts without changing their agent's harness or model. ## Linked Issues or Issue Description **Problem or motivation** AI credentials are configured separately from Connections. Agents cannot consistently reuse a responsible user's account or a permitted shared account. **Proposed solution** Manage AI accounts with the existing Connections grants and permissions. Keep model and harness selection independent from credential selection. Preserve legacy authentication until validated adoption. **Alternatives considered** A separate credential registry would duplicate ownership and access policy. Automatic fallback would risk using the wrong account. **Roadmap alignment** This extends the shipped Apps, multi-user, secrets, and agent-runtime capabilities. The maintainer requested the feature and reviewed the UI. Related groundwork: #11899 (connection permissions), #10910 (connection wizard), #11692 (Claude subscription profiles), and #11854 (Codex account rotation). ## What Changed - Add compact AI-account management to the existing Connectors pages. - Reuse AgentProviderConnection, AdapterLoginPanel, AdapterLoginChrome, and authentication controllers. - Add the shared connection picker to agent setup/settings and task requests. - Preserve onboarding's sequence and reuse existing accounts. - Add local-login recovery, retry, cancellation, and React StrictMode handling. - Add interactive Storybook scenarios, design-guide examples, and app acceptance checks. This is part 2 of the AI Connections change. The runtime foundation in #13247 is merged. This PR now targets master. ## Verification - Updated against master `47ded8bf9`, including the landed runtime foundation and upstream task-search changes. - Full workspace typecheck, production build, Storybook build, and token gates passed on the integrated branch. Final local-login changes passed 59 focused tests; new-agent and inbox regression suites passed 63 tests. - Browser checks verified automatic local Claude account detection, resumable Codex login commands, retry, focus restoration, and desktop/phone layouts. Commands create their isolated directory before invoking the CLI. - All CI test, browser, build, packaging, and runner jobs passed on final head `dd17d3211931dd70aaa6ea619d83a7f9966dd18e`. The fresh Greptile review is 5/5, the security scan passed, and there are no unresolved review threads. The final CI aggregate gates passed. - Local general-server coverage passed 11,804 tests; three port-collision failures passed in an isolated 25-test rerun. All 6,111 UI tests passed. CLI coverage passed 484 tests; its remaining doctor test requires port 3199, which is occupied by an unrelated report server on this Mac. The complete CLI suite passed in CI. - Live browser testing verified Codex API-key reconnect inside a task card on desktop and phone. Real provider runs resumed and completed with unchanged connection/grant identity and agent routing. - Tested opening, cancelling, reopening, and completing connection creation. A regression confirms Connect another account cannot submit the new-agent form or copy provider keys into agent settings. - Added shared inline repair, automatic local sign-in checks, and responsive connection dialogs. Standalone Daytona installation ignores workspace configuration and suppresses dependency scripts. Its standalone build also passed with CI's exact pnpm 9.15.4. - Destructive live tests are excluded by default. Explicit opt-in, local deployment checks, and matching disposable fixture identities are required before any mutation. ## Risks - Local Codex/Grok creation requires the connection-specific terminal login command. - Browser sign-in uses the existing supported-environment controllers. - This update verifies live local Claude detection and Codex API-key task repair. New subscription authorization/refresh and independent-human/native-runner isolation were not reverified in this update. - No agent automatically adopts managed Connections. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, code execution, and browser testing. The exact runtime model identifier and context-window size are not exposed in this session. ## 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>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
import { aiConnectionsApi } from "@/api/ai-connections";
|
|
import type { AiProvider } from "@paperclipai/shared";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { agentsApi } from "@/api/agents";
|
|
import { ApiError } from "@/api/client";
|
|
import { secretsApi } from "@/api/secrets";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import {
|
|
savedProviderKeys,
|
|
savedManagedProviderAccounts,
|
|
savedCodexSubscriptions,
|
|
type SavedProviderKey,
|
|
} from "@/lib/saved-provider-credentials";
|
|
|
|
export function useSavedProviderKeys(
|
|
companyId: string | null,
|
|
envKey: string,
|
|
enabled = true,
|
|
) {
|
|
const provider = ({ ANTHROPIC_API_KEY: "anthropic", OPENAI_API_KEY: "openai", OPENROUTER_API_KEY: "openrouter", XAI_API_KEY: "xai" } as Record<string, AiProvider>)[envKey];
|
|
const managed = useQuery({
|
|
queryKey: ["ai-connections", companyId],
|
|
queryFn: () => aiConnectionsApi.list(companyId!),
|
|
enabled: Boolean(companyId && provider) && enabled,
|
|
retry: false,
|
|
});
|
|
const managedAccounts = provider && managed.data ? savedManagedProviderAccounts(companyId!, provider, managed.data.currentUserId, managed.data.connections) : [];
|
|
const personal = useQuery({
|
|
queryKey: queryKeys.secrets.myUserSecrets(companyId ?? ""),
|
|
queryFn: () => secretsApi.listMyUserSecrets(companyId!),
|
|
enabled: Boolean(companyId) && enabled,
|
|
retry: false,
|
|
});
|
|
const organization = useQuery({
|
|
queryKey: queryKeys.secrets.list(companyId ?? ""),
|
|
queryFn: () => secretsApi.list(companyId!),
|
|
enabled: Boolean(companyId) && enabled,
|
|
retry: false,
|
|
});
|
|
const storedLogin = useQuery({
|
|
// Disabled queries still return cached data. Keep other providers away
|
|
// from the shared Claude login cache.
|
|
queryKey: ["claude-oauth-token-status", envKey === "ANTHROPIC_API_KEY" ? companyId : null],
|
|
queryFn: async () => {
|
|
try {
|
|
return await agentsApi.getClaudeOAuthTokenStatus(companyId!);
|
|
} catch (error) {
|
|
if (error instanceof ApiError && error.status === 404) return null;
|
|
throw error;
|
|
}
|
|
},
|
|
enabled: Boolean(companyId) && enabled && envKey === "ANTHROPIC_API_KEY",
|
|
retry: false,
|
|
});
|
|
return {
|
|
storedLogin,
|
|
options: [...managedAccounts.filter(account => account.aiConnection?.method === "api_key"), ...savedProviderKeys(
|
|
companyId ?? "",
|
|
envKey,
|
|
personal.data ?? [],
|
|
organization.data ?? [],
|
|
)],
|
|
subscriptions: [...managedAccounts.filter(account => account.aiConnection?.method === "subscription"), ...(provider === "openai" ? savedCodexSubscriptions(
|
|
companyId ?? "",
|
|
organization.data ?? [],
|
|
) : [])],
|
|
// Background refreshes must not unmount an active login panel sharing this query.
|
|
loading: personal.isLoading || organization.isLoading || storedLogin.isLoading || managed.isLoading,
|
|
error: personal.isError || organization.isError || managed.isError,
|
|
};
|
|
}
|
|
|
|
export function SavedProviderKeySelect({
|
|
options,
|
|
value,
|
|
onChange,
|
|
loading,
|
|
error,
|
|
disabled,
|
|
kind = "api",
|
|
}: {
|
|
options: SavedProviderKey[];
|
|
value: string;
|
|
onChange: (id: string) => void;
|
|
loading: boolean;
|
|
error: boolean;
|
|
disabled?: boolean;
|
|
kind?: "api" | "subscription";
|
|
}) {
|
|
return (
|
|
<div className="space-y-2">
|
|
{options.length > 0 && (
|
|
<label className="block space-y-2 text-sm">
|
|
<span>{kind === "api" ? "API key" : "Subscription"}</span>
|
|
<select
|
|
aria-label={kind === "api" ? "Saved API key" : "Saved subscription"}
|
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
value={value}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
disabled={disabled}
|
|
>
|
|
{options.map((option) => (
|
|
<option key={option.id} value={option.id}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
<option value="">
|
|
{kind === "api"
|
|
? "Enter a new API key"
|
|
: "Sign in to another account"}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
)}
|
|
{loading && (
|
|
<p role="status" className="text-sm text-muted-foreground">
|
|
Checking saved API keys…
|
|
</p>
|
|
)}
|
|
{error && (
|
|
<p role="alert" className="text-sm text-destructive">
|
|
Some saved keys could not be loaded. You can still enter a new key.
|
|
</p>
|
|
)}
|
|
{value && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Reuse this saved {kind === "api" ? "key" : "subscription"} for this
|
|
agent.
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|