From dc791ec9842ae1937f13dbb710b357d05c4c3375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 21 May 2026 21:34:18 -0400 Subject: [PATCH] Replace one-secret provider dialog with per-auth-type fields OAuth, account, and cloud providers previously fell into the same password input as API key providers, which made them impossible to connect properly. Split the connect dialog by auth type: - api_key: secret + model (unchanged) - oauth_pkce / account_subscription: button that opens the provider's official sign-in URL in a session-partitioned BrowserWindow, then a paste field for the access token returned by the provider - cloud: multi-field form (base URL, region, project, deployment, credential) serialized into the encrypted secret payload Each catalog entry now carries signInUrl and credentialHelp so the dialog can render the right instructions without baking copy into the renderer. Backed by a new launchProviderSignIn IPC method on ProviderService that opens a hardened popup (nodeIntegration off, contextIsolation on, sandbox on, permission denies) and records an audit event. --- AgentCompany-New/src/main/app/ipc-handlers.ts | 8 + .../src/main/services/provider-service.ts | 44 +++++ AgentCompany-New/src/preload/index.ts | 1 + .../src/renderer/src/pages/SettingsPage.tsx | 10 +- .../src/pages/settings/ProvidersPanel.tsx | 158 ++++++++++++++++-- AgentCompany-New/src/shared/commands.ts | 5 + AgentCompany-New/src/shared/desktop-api.ts | 2 + AgentCompany-New/src/shared/domain.ts | 2 + AgentCompany-New/src/shared/ipc.ts | 1 + .../src/shared/provider-catalog.ts | 106 +++++++----- 10 files changed, 283 insertions(+), 54 deletions(-) diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index a400db48..93c2ca5e 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -78,6 +78,14 @@ export function registerIpcHandlers(deps: IpcDeps): void { ipcMain.handle(IPC_CHANNELS.sendMessage, (_event, input) => mutate(() => chat.sendMessage(input))); ipcMain.handle(IPC_CHANNELS.markChatSpaceRead, (_event, input) => mutate(() => chat.markSpaceRead(input))); ipcMain.handle(IPC_CHANNELS.saveProviderCredential, (_event, input) => mutate(() => providers.saveCredential(input))); + ipcMain.handle(IPC_CHANNELS.launchProviderSignIn, (_event, input) => { + try { + providers.launchSignInWindow(input.orgId, input.providerId); + return ok(null); + } catch (error) { + return fail("provider_sign_in_failed", error instanceof Error ? error.message : String(error)); + } + }); ipcMain.handle(IPC_CHANNELS.createProviderResponse, async (_event, input) => { try { await providers.createResponse(input); diff --git a/AgentCompany-New/src/main/services/provider-service.ts b/AgentCompany-New/src/main/services/provider-service.ts index 1fcb7c80..31cc185e 100644 --- a/AgentCompany-New/src/main/services/provider-service.ts +++ b/AgentCompany-New/src/main/services/provider-service.ts @@ -1,7 +1,9 @@ import type { DatabaseSync } from "node:sqlite"; import { createHash } from "node:crypto"; +import { BrowserWindow, session } from "electron"; import type { CreateProviderResponseInput, SaveProviderCredentialInput } from "../../shared/commands"; import { PROVIDER_AUTH_TYPE, PROVIDER_REQUEST_STATE } from "../../shared/domain"; +import { PROVIDER_CATALOG } from "../../shared/provider-catalog"; import { createId } from "../../shared/ids"; import { AuditService } from "./audit-service"; import { SecretVault } from "./secret-vault"; @@ -54,6 +56,48 @@ export class ProviderService { }); } + launchSignInWindow(orgId: string, providerId: string): void { + const provider = this.db.prepare(` + select id, provider_key, label, auth_type + from providers + where id = ? and org_id = ? + `).get(providerId, orgId) as { id: string; provider_key: string; label: string; auth_type: string } | undefined; + if (!provider) throw new Error("Provider was not found."); + const catalogEntry = PROVIDER_CATALOG.find((entry) => entry.key === provider.provider_key); + if (!catalogEntry) throw new Error(`Provider catalog entry missing for ${provider.provider_key}.`); + if (!catalogEntry.signInUrl) throw new Error(`${provider.label} does not support browser sign-in.`); + + const partition = `persist:agentcompany-provider-${provider.id}`; + const providerSession = session.fromPartition(partition); + providerSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false)); + providerSession.setPermissionCheckHandler(() => false); + + const win = new BrowserWindow({ + width: 1100, + height: 760, + title: provider.label, + webPreferences: { + session: providerSession, + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + webSecurity: true, + }, + }); + win.webContents.setWindowOpenHandler(() => ({ action: "deny" })); + win.loadURL(catalogEntry.signInUrl); + + this.audit.record({ + orgId, + entityType: "provider", + entityId: provider.id, + actorType: "employee", + actorId: null, + action: "sign_in_window_opened", + payload: { providerKey: provider.provider_key, authType: provider.auth_type }, + }); + } + async createResponse(input: CreateProviderResponseInput): Promise { const prompt = input.prompt.trim(); if (!prompt) throw new Error("Prompt is required."); diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 80615297..4bc996bf 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -21,6 +21,7 @@ const api: DesktopApi = { sendMessage: (input) => invoke(IPC_CHANNELS.sendMessage, input), markChatSpaceRead: (input) => invoke(IPC_CHANNELS.markChatSpaceRead, input), saveProviderCredential: (input) => invoke(IPC_CHANNELS.saveProviderCredential, input), + launchProviderSignIn: (input) => invoke(IPC_CHANNELS.launchProviderSignIn, input), createProviderResponse: (input) => invoke(IPC_CHANNELS.createProviderResponse, input), importSkillDirectory: (input) => invoke(IPC_CHANNELS.importSkillDirectory, input), bindSkill: (input) => invoke(IPC_CHANNELS.bindSkill, input), diff --git a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx index 0a588e21..1168ac69 100644 --- a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx @@ -84,7 +84,15 @@ export function SettingsPage({ snapshot, actions }: PageProps) { {tab === "members" && } {tab === "billing" && } {tab === "models" && } - {tab === "providers" && } + {tab === "providers" && ( + + )} {tab === "skills" && ( (null); + const [signInLaunched, setSignInLaunched] = useState(false); const connected = providers.filter((provider) => provider.enabled); const available = providers.filter((provider) => !provider.enabled); + const activeCatalogEntry = active ? catalog.find((entry) => entry.key === active.providerKey) ?? null : null; + + const close = () => { + setActive(null); + setSignInLaunched(false); + }; const save = (event: FormEvent) => { event.preventDefault(); - if (!active) return; + if (!active || !activeCatalogEntry) return; const form = new FormData(event.currentTarget); + const secret = composeSecret(activeCatalogEntry, form); void actions.run(() => window.agentCompany.saveProviderCredential({ orgId, providerId: active.id, - secret: String(form.get("secret") ?? "").trim(), + secret, model: String(form.get("model") ?? "").trim() || null, - })).then((result) => { if (result) setActive(null); }); + })).then((result) => { if (result) close(); }); + }; + + const launchSignIn = () => { + if (!active) return; + setSignInLaunched(true); + void actions.run(() => window.agentCompany.launchProviderSignIn({ orgId, providerId: active.id })); }; return ( @@ -61,16 +89,126 @@ export function ProvidersPanel({ orgId, providers, agents, actions }: ProvidersP )} - setActive(null)} onSubmit={save}> - - - - + + {active && activeCatalogEntry && ( + + )} ); } +function ProviderConnectFields({ + provider, + entry, + signInLaunched, + onLaunchSignIn, +}: { + provider: ProviderRecord; + entry: ProviderCatalogEntry; + signInLaunched: boolean; + onLaunchSignIn: () => void; +}) { + if (entry.authType === "api_key") { + return ( + <> +

{entry.credentialHelp}

+ + + + ); + } + if (entry.authType === "oauth_pkce" || entry.authType === "account_subscription") { + return ( + <> +

{entry.credentialHelp}

+
+ + {entry.signInUrl && {entry.signInUrl}} +
+ + + + ); + } + return ; +} + +function CloudFieldsForm({ entry, provider }: { entry: ProviderCatalogEntry; provider: ProviderRecord }) { + const [state, setState] = useState(CLOUD_DEFAULTS); + const update = (patch: Partial) => setState((current) => ({ ...current, ...patch })); + return ( + <> +

{entry.credentialHelp}

+ + update({ baseUrl: event.target.value })} + placeholder="https://" + /> + + + update({ region: event.target.value })} + /> + + + update({ projectId: event.target.value })} + /> + + + update({ deployment: event.target.value })} + /> + + + + + + + + + ); +} + +function composeSecret(entry: ProviderCatalogEntry, form: FormData): string { + const raw = String(form.get("secret") ?? "").trim(); + if (entry.authType !== "cloud") return raw; + const payload = { + baseUrl: String(form.get("cloudBaseUrl") ?? "").trim(), + region: String(form.get("cloudRegion") ?? "").trim(), + projectId: String(form.get("cloudProject") ?? "").trim(), + deployment: String(form.get("cloudDeployment") ?? "").trim(), + secret: raw, + }; + return JSON.stringify(payload); +} + function ProviderRow({ provider, agents, diff --git a/AgentCompany-New/src/shared/commands.ts b/AgentCompany-New/src/shared/commands.ts index e9133cb0..8ad861ba 100644 --- a/AgentCompany-New/src/shared/commands.ts +++ b/AgentCompany-New/src/shared/commands.ts @@ -110,6 +110,11 @@ export interface SaveProviderCredentialInput { model: string | null; } +export interface LaunchProviderSignInInput { + orgId: string; + providerId: string; +} + export interface CreateProviderResponseInput { orgId: string; providerId: string; diff --git a/AgentCompany-New/src/shared/desktop-api.ts b/AgentCompany-New/src/shared/desktop-api.ts index ae080903..77e98d63 100644 --- a/AgentCompany-New/src/shared/desktop-api.ts +++ b/AgentCompany-New/src/shared/desktop-api.ts @@ -18,6 +18,7 @@ import type { DeleteOrganizationInput, ImportFileInput, ImportSkillDirectoryInput, + LaunchProviderSignInInput, LaunchSocialLoginInput, ResolveAgentSkillsInput, SaveProviderCredentialInput, @@ -47,6 +48,7 @@ export interface DesktopApi { sendMessage(input: SendMessageInput): Promise>; markChatSpaceRead(input: MarkChatSpaceReadInput): Promise>; saveProviderCredential(input: SaveProviderCredentialInput): Promise>; + launchProviderSignIn(input: LaunchProviderSignInInput): Promise>; createProviderResponse(input: CreateProviderResponseInput): Promise>; importSkillDirectory(input: ImportSkillDirectoryInput): Promise>; bindSkill(input: BindSkillInput): Promise>; diff --git a/AgentCompany-New/src/shared/domain.ts b/AgentCompany-New/src/shared/domain.ts index 91cc4ef9..5395c3fb 100644 --- a/AgentCompany-New/src/shared/domain.ts +++ b/AgentCompany-New/src/shared/domain.ts @@ -279,6 +279,8 @@ export interface ProviderCatalogEntry { authType: ProviderAuthType; defaultModel: string | null; capabilityTags: string[]; + signInUrl: string | null; + credentialHelp: string; } export interface ProviderRequestRecord { diff --git a/AgentCompany-New/src/shared/ipc.ts b/AgentCompany-New/src/shared/ipc.ts index 411f4ae1..3d050bfd 100644 --- a/AgentCompany-New/src/shared/ipc.ts +++ b/AgentCompany-New/src/shared/ipc.ts @@ -13,6 +13,7 @@ export const IPC_CHANNELS = { sendMessage: "agentcompany:send-message", markChatSpaceRead: "agentcompany:mark-chat-space-read", saveProviderCredential: "agentcompany:save-provider-credential", + launchProviderSignIn: "agentcompany:launch-provider-sign-in", createProviderResponse: "agentcompany:create-provider-response", importSkillDirectory: "agentcompany:import-skill-directory", bindSkill: "agentcompany:bind-skill", diff --git a/AgentCompany-New/src/shared/provider-catalog.ts b/AgentCompany-New/src/shared/provider-catalog.ts index 0fe75dba..4a2e77ed 100644 --- a/AgentCompany-New/src/shared/provider-catalog.ts +++ b/AgentCompany-New/src/shared/provider-catalog.ts @@ -7,6 +7,8 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ authType: PROVIDER_AUTH_TYPE.API_KEY, defaultModel: "gpt-5.1", capabilityTags: ["responses", "tools", "background"], + signInUrl: null, + credentialHelp: "Generate an API key at platform.openai.com → API keys.", }, { key: "anthropic", @@ -14,6 +16,8 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ authType: PROVIDER_AUTH_TYPE.API_KEY, defaultModel: "claude-sonnet-4-5", capabilityTags: ["messages", "tool-use", "token-bucket"], + signInUrl: null, + credentialHelp: "Generate an API key at console.anthropic.com → API keys.", }, { key: "xai", @@ -21,54 +25,70 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] = [ authType: PROVIDER_AUTH_TYPE.API_KEY, defaultModel: "grok-4", capabilityTags: ["chat", "reasoning"], - }, - { - key: "github-copilot", - label: "GitHub Copilot", - authType: PROVIDER_AUTH_TYPE.OAUTH_PKCE, - defaultModel: null, - capabilityTags: ["account", "coding"], - }, - { - key: "google-antigravity", - label: "Google Antigravity", - authType: PROVIDER_AUTH_TYPE.OAUTH_PKCE, - defaultModel: null, - capabilityTags: ["account", "coding"], - }, - { - key: "gpt-account", - label: "GPT account", - authType: PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION, - defaultModel: null, - capabilityTags: ["codex", "subscription"], - }, - { - key: "azure-openai", - label: "Azure OpenAI", - authType: PROVIDER_AUTH_TYPE.CLOUD, - defaultModel: null, - capabilityTags: ["enterprise", "regional"], - }, - { - key: "aws-bedrock", - label: "AWS Bedrock", - authType: PROVIDER_AUTH_TYPE.CLOUD, - defaultModel: null, - capabilityTags: ["enterprise", "multi-model"], - }, - { - key: "vertex-ai", - label: "Vertex AI", - authType: PROVIDER_AUTH_TYPE.CLOUD, - defaultModel: null, - capabilityTags: ["enterprise", "regional"], + signInUrl: null, + credentialHelp: "Generate an API key at console.x.ai → API keys.", }, { key: "groq", label: "Groq", authType: PROVIDER_AUTH_TYPE.API_KEY, - defaultModel: null, + defaultModel: "groq/compound", capabilityTags: ["inference", "low-latency"], + signInUrl: null, + credentialHelp: "Generate an API key at console.groq.com → API keys.", + }, + { + key: "github-copilot", + label: "GitHub Copilot", + authType: PROVIDER_AUTH_TYPE.OAUTH_PKCE, + defaultModel: "claude-opus-4-5", + capabilityTags: ["account", "coding"], + signInUrl: "https://github.com/login", + credentialHelp: "Sign in, then copy the personal access token (scope copilot) from github.com → Settings → Developer settings.", + }, + { + key: "google-antigravity", + label: "Google Antigravity", + authType: PROVIDER_AUTH_TYPE.OAUTH_PKCE, + defaultModel: "claude-sonnet-4-5", + capabilityTags: ["account", "coding"], + signInUrl: "https://antigravity.google/", + credentialHelp: "Sign in to Antigravity, then paste the session token shown in the dashboard.", + }, + { + key: "gpt-account", + label: "GPT account", + authType: PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION, + defaultModel: "gpt-5.3-codex", + capabilityTags: ["codex", "subscription"], + signInUrl: "https://chatgpt.com/codex", + credentialHelp: "Sign in to ChatGPT Plus/Pro, then paste the session access token from chatgpt.com/codex/settings.", + }, + { + key: "azure-openai", + label: "Azure OpenAI", + authType: PROVIDER_AUTH_TYPE.CLOUD, + defaultModel: "gpt-5.4", + capabilityTags: ["enterprise", "regional"], + signInUrl: null, + credentialHelp: "Set base URL (e.g. https://.openai.azure.com), deployment name, and the api-key from Azure portal.", + }, + { + key: "aws-bedrock", + label: "AWS Bedrock", + authType: PROVIDER_AUTH_TYPE.CLOUD, + defaultModel: "anthropic.claude-sonnet-4-5", + capabilityTags: ["enterprise", "multi-model"], + signInUrl: null, + credentialHelp: "Provide AWS region plus an access key + secret access key with bedrock:InvokeModel permission.", + }, + { + key: "vertex-ai", + label: "Vertex AI", + authType: PROVIDER_AUTH_TYPE.CLOUD, + defaultModel: "gemini-2.5-pro", + capabilityTags: ["enterprise", "regional"], + signInUrl: null, + credentialHelp: "Provide GCP project id, region, and a service-account JSON key with aiplatform.user role.", }, ];