diff --git a/src/cliproxy/account-safety.ts b/src/cliproxy/account-safety.ts index 8c9c2136..f7a89cde 100644 --- a/src/cliproxy/account-safety.ts +++ b/src/cliproxy/account-safety.ts @@ -16,8 +16,13 @@ import { CLIProxyProvider } from './types'; import { loadAccountsRegistry, pauseAccount, resumeAccount } from './accounts/registry'; import { getCcsDir } from '../utils/config-manager'; +const ISSUE_509_URL = 'https://github.com/kaitranntt/ccs/issues/509'; + /** Providers that use Google OAuth (ban risk when overlapping) */ const GOOGLE_OAUTH_PROVIDERS: CLIProxyProvider[] = ['gemini', 'agy', 'codex']; +/** Providers that should display direct CLI warnings for #509 */ +const BAN_WARNING_PROVIDERS: CLIProxyProvider[] = ['gemini', 'agy']; +const shownBanWarnings = new Set(); // --- Auto-pause persistence (crash recovery) --- @@ -161,6 +166,10 @@ export function warnCrossProviderDuplicates(provider: CLIProxyProvider): boolean console.error(''); console.error(warn('Account safety: cross-provider duplicate detected')); console.error(' Same Google account across providers risks account bans (ref: #509).'); + console.error( + ' If provider requests start returning 403/Forbidden, treat it as a possible ban.' + ); + console.error(` Details: ${ISSUE_509_URL}`); console.error(''); for (const [email, providers] of duplicates) { @@ -188,10 +197,67 @@ export function warnNewAccountConflict( ` ${maskEmail(email)} is also registered under: ${conflictingProviders.join(', ')}` ); console.error(' Concurrent usage may cause Google to ban your account.'); + console.error(' 403/Forbidden responses can be an early sign of account disablement.'); console.error(' Consider pausing the duplicate or using a different account.'); + console.error(` Details: ${ISSUE_509_URL}`); console.error(''); } +function isBanWarningProvider(provider: CLIProxyProvider): boolean { + return BAN_WARNING_PROVIDERS.includes(provider); +} + +/** + * Show one-time warning for known OAuth ban risk providers. + */ +export function warnOAuthBanRisk(provider: CLIProxyProvider): void { + if (!isBanWarningProvider(provider) || shownBanWarnings.has(provider)) return; + + shownBanWarnings.add(provider); + console.error(''); + console.error(warn('Account safety warning (#509)')); + console.error( + ' Using the same Google account in both "ccs gemini" and "ccs agy" can trigger suspension.' + ); + console.error( + ' If you see 403/Forbidden during provider calls, treat it as likely account disable/ban.' + ); + console.error( + ' Use separate Google accounts per provider and stop retrying blocked accounts.' + ); + console.error(` Details: ${ISSUE_509_URL}`); + console.error(''); +} + +/** + * Detect whether an error message contains a likely 403/Forbidden ban signal. + */ +export function isPossible403BanSignal(errorMessage: string): boolean { + const lower = errorMessage.toLowerCase(); + return lower.includes('403') || lower.includes('forbidden'); +} + +/** + * Show targeted warning when OAuth provider errors include 403/Forbidden. + * Returns true when warning was emitted. + */ +export function warnPossible403Ban(provider: CLIProxyProvider, errorMessage: string): boolean { + if (!isBanWarningProvider(provider) || !isPossible403BanSignal(errorMessage)) { + return false; + } + + console.error(''); + console.error(warn(`Account safety: ${provider} returned 403/Forbidden`)); + console.error( + ' For gemini/agy flows this often means the Google account was blocked/disabled.' + ); + console.error(' Stop retries for this account and switch to a different account/provider.'); + console.error(` Details: ${ISSUE_509_URL}`); + console.error(` Error: "${truncate(errorMessage, 160)}"`); + console.error(''); + return true; +} + // --- Enforcement: auto-pause/restore --- /** diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index dc651216..f05316d2 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -44,7 +44,12 @@ import { getProviderTokenDir, isAuthenticated, registerAccountFromToken } from ' import { executeOAuthProcess } from './oauth-process'; import { importKiroToken } from './kiro-import'; import { getProxyTarget, buildProxyUrl, buildManagementHeaders } from '../proxy-target-resolver'; -import { checkNewAccountConflict, warnNewAccountConflict } from '../account-safety'; +import { + checkNewAccountConflict, + warnNewAccountConflict, + warnOAuthBanRisk, + warnPossible403Ban, +} from '../account-safety'; /** * Prompt user to add another account @@ -278,7 +283,9 @@ async function handlePasteCallbackMode( }); if (!startResponse.ok) { + const startError = `OAuth start failed with status ${startResponse.status}`; console.log(fail('Failed to start OAuth flow')); + warnPossible403Ban(provider, startError); return null; } @@ -380,7 +387,10 @@ async function handlePasteCallbackMode( }; if (!callbackResponse.ok || callbackData.status === 'error') { - console.log(fail(callbackData.error || 'OAuth callback failed')); + const callbackError = + callbackData.error || `OAuth callback failed with status ${callbackResponse.status}`; + console.log(fail(callbackError)); + warnPossible403Ban(provider, callbackError); return null; } @@ -417,6 +427,7 @@ export async function triggerOAuth( options: OAuthOptions = {} ): Promise { const oauthConfig = getOAuthConfig(provider); + warnOAuthBanRisk(provider); const { verbose = false, add = false, fromUI = false, noIncognito = true } = options; let { nickname } = options; const resolvedKiroMethod = diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 3ea5b1c7..edc701e4 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -67,6 +67,7 @@ import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './ import { parseThinkingOverride } from './thinking-arg-parser'; import { warnCrossProviderDuplicates, + warnOAuthBanRisk, cleanupStaleAutoPauses, enforceProviderIsolation, restoreAutoPausedAccounts, @@ -187,6 +188,7 @@ export async function execClaudeWithCLIProxy( const providerConfig = getProviderConfig(provider); log(`Provider: ${providerConfig.displayName}`); + warnOAuthBanRisk(provider); // Check remote proxy if configured let useRemoteProxy = false; diff --git a/src/cliproxy/executor/retry-handler.ts b/src/cliproxy/executor/retry-handler.ts index 514af33f..7149cdad 100644 --- a/src/cliproxy/executor/retry-handler.ts +++ b/src/cliproxy/executor/retry-handler.ts @@ -10,7 +10,7 @@ import { fail, warn, info } from '../../utils/ui'; import { CLIProxyProvider } from '../types'; -import { handleBanDetection } from '../account-safety'; +import { handleBanDetection, warnPossible403Ban } from '../account-safety'; import { CompositeTierConfig } from '../../config/unified-config-types'; /** @@ -59,6 +59,7 @@ export async function handleTokenExpiration( if (account) { handleBanDetection(provider, account.id, tokenResult.error); } + warnPossible403Ban(provider, tokenResult.error); } // Token expired and refresh failed - trigger re-auth diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 3640565f..583b3355 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -8,9 +8,10 @@ import { useState, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; -import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2 } from 'lucide-react'; +import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2, AlertTriangle } from 'lucide-react'; import { QuickSetupWizard } from '@/components/quick-setup-wizard'; import { AddAccountDialog } from '@/components/account/add-account-dialog'; import { ProviderEditor } from '@/components/cliproxy/provider-editor'; @@ -243,6 +244,10 @@ export function CliproxyPage() { const parentAuthForVariant = selectedVariantData ? providers.find((p) => p.provider === selectedVariantData.provider) : undefined; + const warningProvider = (selectedVariantData?.provider || selectedStatus?.provider || '') + .toLowerCase() + .trim(); + const showAccountSafetyWarning = warningProvider === 'gemini' || warningProvider === 'agy'; const handleRefresh = () => { queryClient.invalidateQueries({ queryKey: ['cliproxy'] }); @@ -389,6 +394,30 @@ export function CliproxyPage() { {/* Right Panel */}
+ {showAccountSafetyWarning && ( +
+ + + Account Safety Warning (Issue #509) + + Using the same Google account in both ccs gemini{' '} + and ccs agy can trigger suspension. If CLI shows{' '} + 403 Forbidden, treat it as likely account + disable/ban and switch accounts.{' '} + + Read issue #509 + + . + + +
+ )} + {selectedVariantData && parentAuthForVariant ? ( // Variant selected - show ProviderEditor with variant profile name