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.", }, ];