From d19573187c3eb4b8a47a60af8f5e0d8b9f06f4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 7 Jul 2026 01:53:22 -0400 Subject: [PATCH] Fix connector Connect: use the link flow and surface failures Root cause found by probing the live API: Composio v3 rejects POST /connected_accounts for Composio-managed OAuth auth configs with a 400 pointing at POST /connected_accounts/link, and the renderer threw the failure away inside a .then, so clicking Connect did nothing and no browser opened. - createConnectedAccount now calls POST /connected_accounts/link with auth_config_id and user_id and reads connected_account_id and redirect_url; the local mirror records INITIALIZING until the poll adopts Composio's real status. - Connect failures (and a blocked external-URL open) now show as a single line on the page instead of vanishing. --- src/main/services/composio/composio-client.ts | 16 +++++----- .../composio/composio-connection-service.ts | 10 ++++--- src/renderer/src/pages/ConnectorsPage.tsx | 29 ++++++++++++++++--- src/shared/commands/composio.ts | 1 - 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/main/services/composio/composio-client.ts b/src/main/services/composio/composio-client.ts index 1ade5500..1da70d39 100644 --- a/src/main/services/composio/composio-client.ts +++ b/src/main/services/composio/composio-client.ts @@ -94,19 +94,21 @@ export class ComposioClient { return id; } - /** Starts a connection for a user; the redirect URL completes it in the browser. */ + /** + * Starts a connection for a user; the redirect URL completes it in the + * browser. Composio-managed auth configs only accept the link flow — + * POST /connected_accounts is rejected for them with a 400. + */ async createConnectedAccount(input: { authConfigId: string; userId: string }): Promise<{ id: string; - status: string; redirectUrl: string | null; }> { - const response = await this.request("POST", `${BASE_URL}/connected_accounts`, { - auth_config: { id: input.authConfigId }, - connection: { user_id: input.userId }, + const response = await this.request("POST", `${BASE_URL}/connected_accounts/link`, { + auth_config_id: input.authConfigId, + user_id: input.userId, }); return { - id: String(response.id ?? ""), - status: String(response.status ?? ""), + id: String(response.connected_account_id ?? ""), redirectUrl: typeof response.redirect_url === "string" ? response.redirect_url : null, }; } diff --git a/src/main/services/composio/composio-connection-service.ts b/src/main/services/composio/composio-connection-service.ts index bc8265d0..83784cf9 100644 --- a/src/main/services/composio/composio-connection-service.ts +++ b/src/main/services/composio/composio-connection-service.ts @@ -26,8 +26,10 @@ export class ComposioConnectionService { /** * Starts (or restarts) the company's connection for a toolkit: creates a * Composio-managed auth config once per toolkit, initiates the connected - * account for the company user, and records the pending account locally. - * The returned redirect URL completes authorization in the browser. + * account for the company user via the link flow, and records the pending + * account locally as INITIALIZING — the poll that follows adopts Composio's + * real status. The returned redirect URL completes authorization in the + * browser. */ async connectToolkit(orgId: string, toolkitSlug: string, toolkitName: string): Promise { const slug = normalizeSlug(toolkitSlug); @@ -48,8 +50,8 @@ export class ComposioConnectionService { auth_config_id = excluded.auth_config_id, status = excluded.status, updated_at = excluded.updated_at - `).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, account.status || "INITIATED", now, now); - return { accountId: account.id, status: account.status, redirectUrl: account.redirectUrl }; + `).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, "INITIALIZING", now, now); + return { accountId: account.id, redirectUrl: account.redirectUrl }; } /** Pulls account statuses from Composio and updates the local mirror. */ diff --git a/src/renderer/src/pages/ConnectorsPage.tsx b/src/renderer/src/pages/ConnectorsPage.tsx index 3c34b4f6..db1b3f6e 100644 --- a/src/renderer/src/pages/ConnectorsPage.tsx +++ b/src/renderer/src/pages/ConnectorsPage.tsx @@ -26,6 +26,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { const employee = requireActiveEmployee(snapshot); const [catalogSlot, setCatalogSlot] = useState(null); const [busySlug, setBusySlug] = useState(null); + const [connectError, setConnectError] = useState(null); const pollTimer = useRef | null>(null); const catalog: CatalogState = catalogSlot && catalogSlot.orgId === orgId ? catalogSlot.state : { phase: "loading" }; @@ -90,14 +91,19 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { const connect = (toolkit: ComposioToolkitSummaryDto) => { setBusySlug(toolkit.slug); + setConnectError(null); void window.agentCompany .connectComposioToolkit({ orgId, toolkitSlug: toolkit.slug, toolkitName: toolkit.name }) - .then((result) => { - if (!result.ok) throw new Error(result.error.message); - if (result.data.redirectUrl) { - void window.agentCompany.openExternalUrl(result.data.redirectUrl); + .then(async (result) => { + if (!result.ok) { + setConnectError(result.error.message); + return; } pollUntilActive(toolkit.slug, 0); + if (result.data.redirectUrl) { + const opened = await window.agentCompany.openExternalUrl(result.data.redirectUrl); + if (!opened.ok) setConnectError(opened.error.message); + } }) .finally(() => setBusySlug(null)); }; @@ -112,6 +118,21 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) {

Connectors

+ {connectError && ( +
+ {connectError} +
+ )} + {catalog.phase === "loading" && ( diff --git a/src/shared/commands/composio.ts b/src/shared/commands/composio.ts index 365065be..35c61268 100644 --- a/src/shared/commands/composio.ts +++ b/src/shared/commands/composio.ts @@ -21,7 +21,6 @@ export interface ConnectComposioToolkitInput { export interface ConnectComposioToolkitResult { accountId: string; - status: string; redirectUrl: string | null; }