From 9d964c8d91e329e4e4261708037e59a74968a418 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 6 Oct 2026 13:03:17 -0500 Subject: [PATCH] fix(connections): preserve provider routing during task-card repair Co-Authored-By: Paperclip --- doc/connections/AI-CONNECTIONS.md | 11 ++++++++ packages/paperclip-runner/README.md | 6 ----- .../ConnectionIntentInteractionBody.test.tsx | 26 +++++++++++++++++++ .../ConnectionIntentInteractionBody.tsx | 9 ++++++- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/doc/connections/AI-CONNECTIONS.md b/doc/connections/AI-CONNECTIONS.md index 7c009d0acc3..ac56651ea2e 100644 --- a/doc/connections/AI-CONNECTIONS.md +++ b/doc/connections/AI-CONNECTIONS.md @@ -615,3 +615,14 @@ calculation, completed the task, and ran a context-dependent follow-up. Actual tool output was verified for all four successful runs. Private gateways still have deterministic mapping and validation coverage but need live verification in the target deployment. Short-lived Bedrock keys must be rotated before expiry. + +### Gateway completion compatibility + +Provider-facing `paperclip_finish` accepts an omitted or `null` continuation for +`done`, `completed`, and `needs_review`. This supports gateways that require all +declared tool properties to be present. Normalization removes only `null`; +non-yielding tool calls still reject a continuation object, and `yielded` still +requires a complete `response_wake` object. + +Task-card account repair uses the provider reconnect form for routed accounts, +retaining the saved endpoint, protocol, model aliases, and connection identity. diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md index f1d28444d8c..5936d1c7b47 100644 --- a/packages/paperclip-runner/README.md +++ b/packages/paperclip-runner/README.md @@ -66,12 +66,6 @@ effects, audit, retries, conflicts, redaction, continuation, and terminal decisions. The production adapter stays App-owned and invokes Paperclip's real route/service authorities; it does not copy those rules into this package. -Provider-facing `paperclip_finish` accepts an omitted or `null` continuation for -`done`, `completed`, and `needs_review`. This supports gateways that require all -declared tool properties to be present. Normalization removes only `null`; -non-yielding tool calls still reject a continuation object, and `yielded` still -requires a complete `response_wake` object. - ## Quick start Native provider debug-trace correlation uses an incremental index owned by its transport. Pending diff --git a/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx b/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx index 44562e7bcf2..efe20b64724 100644 --- a/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx +++ b/ui/src/features/connections/ConnectionIntentInteractionBody.test.tsx @@ -30,6 +30,7 @@ vi.mock("@/api/email", () => ({ emailApi: { } })); const credentialRender = vi.hoisted(() => vi.fn()); +const routedCredentialRender = vi.hoisted(() => vi.fn()); const setupOptionsMock = vi.hoisted(() => vi.fn()); const completeMock = vi.hoisted(() => vi.fn()); const declineMock = vi.hoisted(() => vi.fn()); @@ -68,6 +69,16 @@ vi.mock("@/components/ai-connections/AiConnectionCredentialStep", () => ({ ; }, })); +vi.mock("@/components/ai-connections/AiProviderSetup", () => ({ + AiProviderSetup: (props: { reconnect: { id: string }; onComplete: (binding: { connectionId: string; grantId: string; method: "api_key" }) => void; onCancel: () => void }) => { + routedCredentialRender(props); + return
+ + +
; + }, +})); + vi.mock("./ConnectionSetupFlow", () => ({ ConnectionSetupFlow: (props: { requestedAgentId?: string; @@ -477,6 +488,21 @@ describe("ConnectionIntentInteractionBody dialog behavior", () => { describe("AI repair inside the card", () => { const interaction: ConnectionIntentInteraction = { ...pendingConnectionIntentInteraction, payload: { ...pendingConnectionIntentInteraction.payload, purpose: "ai" } }; const connection = { id: "selected-account", name: "My Codex account", provider: "openai", method: "api_key", ownership: "personal", ownerName: "Dotta", status: "revoked" }; + it.each(["openrouter", "bedrock", "gateway"] as const)("retains the selected %s route through task-card account repair", async kind => { + const routing = kind === "bedrock" + ? { kind, protocol: "bedrock", region: "us-east-1", auth: "bearer", models: [] } + : { kind, protocol: "responses", auth: "bearer", models: [], ...(kind === "gateway" ? { baseUrl: "https://gateway.example/v1" } : {}) }; + const routedConnection = { ...connection, provider: kind === "bedrock" ? "anthropic" : kind === "openrouter" ? "openrouter" : "openai", routing }; + setupOptionsMock.mockResolvedValue({ interaction, existingConnections: [], aiRepair: { connection: routedConnection, canReconnect: true } }); + completeMock.mockResolvedValue({ ...interaction, status: "accepted" }); + renderBody(interaction); await flush(); + await act(() => button("Fix connection")!.click()); + expect(document.querySelector('[data-testid="shared-ai-credentials"]')).toBeNull(); + expect(routedCredentialRender.mock.lastCall![0]).toMatchObject({ companyId: interaction.companyId, agentId: interaction.payload.requestingAgentId, reconnect: routedConnection }); + await act(() => button("Reconnect routed account")!.click()); + expect(completeMock).toHaveBeenCalledWith(interaction.id, connection.id); + }); + it.each([false, true])("requires atomic validated legacy adoption (validation fails: %s)", async (fails) => { const binding = { provider: "openai", method: "subscription", mode: "responsible_user" }; setupOptionsMock.mockResolvedValue({ interaction, existingConnections: [], aiConnection: binding, aiConnectionRequiresAdoption: true }); diff --git a/ui/src/features/connections/ConnectionIntentInteractionBody.tsx b/ui/src/features/connections/ConnectionIntentInteractionBody.tsx index 21e32b48a38..2fe45ef46f6 100644 --- a/ui/src/features/connections/ConnectionIntentInteractionBody.tsx +++ b/ui/src/features/connections/ConnectionIntentInteractionBody.tsx @@ -14,6 +14,7 @@ import { connectionIntentsApi } from "@/api/connection-intents"; import { aiConnectionsApi } from "@/api/ai-connections"; import { agentsApi } from "@/api/agents"; import { AiConnectionCredentialStep } from "@/components/ai-connections/AiConnectionCredentialStep"; +import { AiProviderSetup } from "@/components/ai-connections/AiProviderSetup"; import { defaultAiConnectionName } from "@/components/ai-connections/model"; import { AppLogo } from "@/pages/apps/AppLogo"; import { AgentAvatar } from "@/components/AgentAvatar"; @@ -393,7 +394,13 @@ export function ConnectionIntentInteractionBody({ {completeMutation.isPending ? "Continuing…" : "Continue task"} - : repair ? repair.canReconnect ? selectAiAccountMutation.mutate({ connectionId: binding.connectionId, grantId: binding.grantId, method: binding.method ?? "api_key", generation })} + onCancel={() => { closeSetup(); returnFocusToCard(); }} + /> :