From c67dd4c034512edcbc3f2480559746697034fe49 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 10 Jun 2026 09:24:22 +0200 Subject: [PATCH] fix(coding-agent): claim MCP OAuth manual input atomically and clear only the owned claim --- .../controllers/mcp-command-controller.ts | 17 +++++++---- .../src/modes/oauth-manual-input.ts | 28 +++++++++++++++++-- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts index 8f292c5ec..6f833d465 100644 --- a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts @@ -597,11 +597,13 @@ export class MCPCommandController { const resolvedClientSecret = clientSecret.trim() || undefined; const manualInput = this.ctx.oauthManualInput; - if (manualInput.hasPending() && manualInput.pendingProviderId !== MCP_MANUAL_INPUT_PROVIDER_ID) { + if (manualInput.hasPending()) { + const pendingProvider = manualInput.pendingProviderId ?? "another provider"; throw new Error( - `OAuth login already in progress for ${manualInput.pendingProviderId}. Complete or cancel it before starting MCP OAuth.`, + `OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`, ); } + let manualInputClaim: { promise: Promise; clear: (reason?: string) => void } | undefined; const oauthTimeout = new AbortController(); try { // Create OAuth flow @@ -658,13 +660,16 @@ export class MCPCommandController { this.ctx.present([new Spacer(1), new Text(theme.fg("muted", message), 1, 0)]); }, onManualCodeInput: () => { - const pendingInput = manualInput.tryWaitForInput(MCP_MANUAL_INPUT_PROVIDER_ID); + if (manualInputClaim) return manualInputClaim.promise; + const pendingInput = manualInput.tryClaimInput(MCP_MANUAL_INPUT_PROVIDER_ID); if (!pendingInput) { + const pendingProvider = manualInput.pendingProviderId ?? "another provider"; throw new Error( - `OAuth login already in progress for ${manualInput.pendingProviderId}. Complete or cancel it before starting MCP OAuth.`, + `OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`, ); } - return pendingInput; + manualInputClaim = pendingInput; + return pendingInput.promise; }, signal: oauthTimeout.signal, }, @@ -716,7 +721,7 @@ export class MCPCommandController { throw new Error(`OAuth authentication failed: ${errorMsg}`); } } finally { - manualInput.clear("Manual MCP OAuth input cleared"); + manualInputClaim?.clear("Manual MCP OAuth input cleared"); } } diff --git a/packages/coding-agent/src/modes/oauth-manual-input.ts b/packages/coding-agent/src/modes/oauth-manual-input.ts index 4591fcb1d..a5d974199 100644 --- a/packages/coding-agent/src/modes/oauth-manual-input.ts +++ b/packages/coding-agent/src/modes/oauth-manual-input.ts @@ -3,6 +3,10 @@ type PendingInput = { resolve: (value: string) => void; reject: (error: Error) => void; }; +type ClaimedInput = { + promise: Promise; + clear: (reason?: string) => void; +}; export class OAuthManualInputManager { #pending?: PendingInput; @@ -12,9 +16,9 @@ export class OAuthManualInputManager { this.clear("Manual OAuth input superseded by a new login"); } - const { promise, resolve, reject } = Promise.withResolvers(); - this.#pending = { providerId, resolve, reject }; - return promise; + const pending = this.#createPending(providerId); + this.#pending = pending; + return pending.promise; } tryWaitForInput(providerId: string): Promise | undefined { @@ -22,6 +26,19 @@ export class OAuthManualInputManager { return this.waitForInput(providerId); } + tryClaimInput(providerId: string): ClaimedInput | undefined { + if (this.#pending) return undefined; + const pending = this.#createPending(providerId); + this.#pending = pending; + return { + promise: pending.promise, + clear: (reason?: string) => { + if (this.#pending !== pending) return; + this.clear(reason); + }, + }; + } + submit(input: string): boolean { if (!this.#pending) return false; const { resolve } = this.#pending; @@ -44,4 +61,9 @@ export class OAuthManualInputManager { get pendingProviderId(): string | undefined { return this.#pending?.providerId; } + + #createPending(providerId: string): PendingInput & { promise: Promise } { + const { promise, resolve, reject } = Promise.withResolvers(); + return { providerId, resolve, reject, promise }; + } }