diff --git a/src/main/ipc/repos-remote.test.ts b/src/main/ipc/repos-remote.test.ts index bd2541c7ebd..7d73f28c436 100644 --- a/src/main/ipc/repos-remote.test.ts +++ b/src/main/ipc/repos-remote.test.ts @@ -103,7 +103,14 @@ vi.mock('../git/repo', async () => { vi.mock('../git/runner', () => ({ gitExecFileAsync: vi.fn(), - gitSpawn: gitSpawnMock + gitSpawn: gitSpawnMock, + // Mirrors the real guard's credential-prompt markers so the clone regression + // test (#7652) can assert Git Credential Manager's OAuth popup is disabled. + nonInteractiveGitEnv: (env: NodeJS.ProcessEnv = process.env) => ({ + ...env, + GIT_TERMINAL_PROMPT: '0', + GCM_INTERACTIVE: 'never' + }) })) vi.mock('../git/worktree', () => ({ @@ -2067,6 +2074,28 @@ describe('repos:add + repos:clone', () => { expect(result).toHaveProperty('path', join(destination, 'orca')) }) + it('clones with the non-interactive credential guard so Git Credential Manager cannot pop its OAuth window (#7652)', async () => { + const destination = await createTempRoot() + + await handlers.get('repos:clone')!(null, { + url: 'https://example.com/orca.git', + destination + }) + + // Without this env, a clone that needs GitHub auth makes Git Credential + // Manager pop its "Connect to GitHub" OAuth window on Windows and loop it + // when the network cannot complete the flow. + expect(gitSpawnMock).toHaveBeenCalledWith( + ['clone', '--progress', '--', 'https://example.com/orca.git', join(destination, 'orca')], + expect.objectContaining({ + env: expect.objectContaining({ + GIT_TERMINAL_PROMPT: '0', + GCM_INTERACTIVE: 'never' + }) + }) + ) + }) + it('treats cloneAbort with no active clone as a no-op', async () => { await expect(handlers.get('repos:cloneAbort')!(null, undefined)).resolves.toBeUndefined() }) diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index c5d1c8f2a9c..8dac54f6670 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -42,7 +42,7 @@ import { isTuiAgent } from '../../shared/tui-agent-config' import { invalidateAuthorizedRootsCache } from './filesystem-auth' import type { ChildProcess } from 'node:child_process' import { access, mkdir, readdir, rm } from 'node:fs/promises' -import { gitExecFileAsync, gitSpawn } from '../git/runner' +import { gitExecFileAsync, gitSpawn, nonInteractiveGitEnv } from '../git/runner' import { isAbsolute, join, posix } from 'node:path' import { cleanupClaimedCloneTarget, @@ -2253,6 +2253,13 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v try { proc = gitSpawn(['clone', '--progress', '--', args.url, clonePath], { cwd: args.destination, + // Why: without the non-interactive guard, a clone that needs + // GitHub auth makes Git Credential Manager pop its "Connect to + // GitHub" OAuth window on Windows; in a network-restricted env the + // browser/device flow can never complete and git's credential + // retry re-pops it (issue #7652). Fail fast with a clear error and + // let Orca's non-intrusive GitHub state stand instead. + env: nonInteractiveGitEnv(), stdio: ['ignore', 'ignore', 'pipe'] }) } catch (err) { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 9681ea33e14..d600720ae63 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -41,7 +41,7 @@ import { AGENT_PROMPT_SUBMIT_DELAY_MS, buildAgentPromptPasteBytes } from '../../shared/agent-prompt-injection' -import { gitExecFileAsync, wslAwareSpawn } from '../git/runner' +import { gitExecFileAsync, wslAwareSpawn, nonInteractiveGitEnv } from '../git/runner' import { cleanupClaimedCloneTarget, claimCloneTarget, @@ -10666,6 +10666,12 @@ export class OrcaRuntimeService { try { proc = wslAwareSpawn('git', ['clone', '--progress', '--', trimmedUrl, clonePath], { cwd: trimmedDestination, + // Why: without the non-interactive guard, a clone that needs GitHub + // auth makes Git Credential Manager pop its "Connect to GitHub" OAuth + // window on Windows; in a network-restricted env the browser/device + // flow can never complete and git's credential retry re-pops it + // (issue #7652). Fail fast with a clear error instead. + env: nonInteractiveGitEnv(), stdio: ['ignore', 'ignore', 'pipe'] }) } catch (err) {