mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-22 22:23:37 +00:00
fix(cliproxy): warn gemini and agy users about issue 509
This commit is contained in:
@@ -16,8 +16,13 @@ import { CLIProxyProvider } from './types';
|
|||||||
import { loadAccountsRegistry, pauseAccount, resumeAccount } from './accounts/registry';
|
import { loadAccountsRegistry, pauseAccount, resumeAccount } from './accounts/registry';
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
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) */
|
/** Providers that use Google OAuth (ban risk when overlapping) */
|
||||||
const GOOGLE_OAUTH_PROVIDERS: CLIProxyProvider[] = ['gemini', 'agy', 'codex'];
|
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<CLIProxyProvider>();
|
||||||
|
|
||||||
// --- Auto-pause persistence (crash recovery) ---
|
// --- Auto-pause persistence (crash recovery) ---
|
||||||
|
|
||||||
@@ -161,6 +166,10 @@ export function warnCrossProviderDuplicates(provider: CLIProxyProvider): boolean
|
|||||||
console.error('');
|
console.error('');
|
||||||
console.error(warn('Account safety: cross-provider duplicate detected'));
|
console.error(warn('Account safety: cross-provider duplicate detected'));
|
||||||
console.error(' Same Google account across providers risks account bans (ref: #509).');
|
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('');
|
console.error('');
|
||||||
|
|
||||||
for (const [email, providers] of duplicates) {
|
for (const [email, providers] of duplicates) {
|
||||||
@@ -188,10 +197,67 @@ export function warnNewAccountConflict(
|
|||||||
` ${maskEmail(email)} is also registered under: ${conflictingProviders.join(', ')}`
|
` ${maskEmail(email)} is also registered under: ${conflictingProviders.join(', ')}`
|
||||||
);
|
);
|
||||||
console.error(' Concurrent usage may cause Google to ban your account.');
|
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(' Consider pausing the duplicate or using a different account.');
|
||||||
|
console.error(` Details: ${ISSUE_509_URL}`);
|
||||||
console.error('');
|
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 ---
|
// --- Enforcement: auto-pause/restore ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ import { getProviderTokenDir, isAuthenticated, registerAccountFromToken } from '
|
|||||||
import { executeOAuthProcess } from './oauth-process';
|
import { executeOAuthProcess } from './oauth-process';
|
||||||
import { importKiroToken } from './kiro-import';
|
import { importKiroToken } from './kiro-import';
|
||||||
import { getProxyTarget, buildProxyUrl, buildManagementHeaders } from '../proxy-target-resolver';
|
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
|
* Prompt user to add another account
|
||||||
@@ -278,7 +283,9 @@ async function handlePasteCallbackMode(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!startResponse.ok) {
|
if (!startResponse.ok) {
|
||||||
|
const startError = `OAuth start failed with status ${startResponse.status}`;
|
||||||
console.log(fail('Failed to start OAuth flow'));
|
console.log(fail('Failed to start OAuth flow'));
|
||||||
|
warnPossible403Ban(provider, startError);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +387,10 @@ async function handlePasteCallbackMode(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!callbackResponse.ok || callbackData.status === 'error') {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +427,7 @@ export async function triggerOAuth(
|
|||||||
options: OAuthOptions = {}
|
options: OAuthOptions = {}
|
||||||
): Promise<AccountInfo | null> {
|
): Promise<AccountInfo | null> {
|
||||||
const oauthConfig = getOAuthConfig(provider);
|
const oauthConfig = getOAuthConfig(provider);
|
||||||
|
warnOAuthBanRisk(provider);
|
||||||
const { verbose = false, add = false, fromUI = false, noIncognito = true } = options;
|
const { verbose = false, add = false, fromUI = false, noIncognito = true } = options;
|
||||||
let { nickname } = options;
|
let { nickname } = options;
|
||||||
const resolvedKiroMethod =
|
const resolvedKiroMethod =
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './
|
|||||||
import { parseThinkingOverride } from './thinking-arg-parser';
|
import { parseThinkingOverride } from './thinking-arg-parser';
|
||||||
import {
|
import {
|
||||||
warnCrossProviderDuplicates,
|
warnCrossProviderDuplicates,
|
||||||
|
warnOAuthBanRisk,
|
||||||
cleanupStaleAutoPauses,
|
cleanupStaleAutoPauses,
|
||||||
enforceProviderIsolation,
|
enforceProviderIsolation,
|
||||||
restoreAutoPausedAccounts,
|
restoreAutoPausedAccounts,
|
||||||
@@ -187,6 +188,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
|
|
||||||
const providerConfig = getProviderConfig(provider);
|
const providerConfig = getProviderConfig(provider);
|
||||||
log(`Provider: ${providerConfig.displayName}`);
|
log(`Provider: ${providerConfig.displayName}`);
|
||||||
|
warnOAuthBanRisk(provider);
|
||||||
|
|
||||||
// Check remote proxy if configured
|
// Check remote proxy if configured
|
||||||
let useRemoteProxy = false;
|
let useRemoteProxy = false;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import { fail, warn, info } from '../../utils/ui';
|
import { fail, warn, info } from '../../utils/ui';
|
||||||
import { CLIProxyProvider } from '../types';
|
import { CLIProxyProvider } from '../types';
|
||||||
import { handleBanDetection } from '../account-safety';
|
import { handleBanDetection, warnPossible403Ban } from '../account-safety';
|
||||||
import { CompositeTierConfig } from '../../config/unified-config-types';
|
import { CompositeTierConfig } from '../../config/unified-config-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,6 +59,7 @@ export async function handleTokenExpiration(
|
|||||||
if (account) {
|
if (account) {
|
||||||
handleBanDetection(provider, account.id, tokenResult.error);
|
handleBanDetection(provider, account.id, tokenResult.error);
|
||||||
}
|
}
|
||||||
|
warnPossible403Ban(provider, tokenResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token expired and refresh failed - trigger re-auth
|
// Token expired and refresh failed - trigger re-auth
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { useState, useMemo } from 'react';
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
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 { QuickSetupWizard } from '@/components/quick-setup-wizard';
|
||||||
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
||||||
import { ProviderEditor } from '@/components/cliproxy/provider-editor';
|
import { ProviderEditor } from '@/components/cliproxy/provider-editor';
|
||||||
@@ -243,6 +244,10 @@ export function CliproxyPage() {
|
|||||||
const parentAuthForVariant = selectedVariantData
|
const parentAuthForVariant = selectedVariantData
|
||||||
? providers.find((p) => p.provider === selectedVariantData.provider)
|
? providers.find((p) => p.provider === selectedVariantData.provider)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const warningProvider = (selectedVariantData?.provider || selectedStatus?.provider || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
const showAccountSafetyWarning = warningProvider === 'gemini' || warningProvider === 'agy';
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
|
||||||
@@ -389,6 +394,30 @@ export function CliproxyPage() {
|
|||||||
|
|
||||||
{/* Right Panel */}
|
{/* Right Panel */}
|
||||||
<div className="flex-1 flex flex-col min-w-0 bg-background">
|
<div className="flex-1 flex flex-col min-w-0 bg-background">
|
||||||
|
{showAccountSafetyWarning && (
|
||||||
|
<div className="px-4 pt-4">
|
||||||
|
<Alert variant="warning" className="py-2">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Account Safety Warning (Issue #509)</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Using the same Google account in both <code className="font-mono">ccs gemini</code>{' '}
|
||||||
|
and <code className="font-mono">ccs agy</code> can trigger suspension. If CLI shows{' '}
|
||||||
|
<code className="font-mono">403 Forbidden</code>, treat it as likely account
|
||||||
|
disable/ban and switch accounts.{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/kaitranntt/ccs/issues/509"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="underline font-medium"
|
||||||
|
>
|
||||||
|
Read issue #509
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedVariantData && parentAuthForVariant ? (
|
{selectedVariantData && parentAuthForVariant ? (
|
||||||
// Variant selected - show ProviderEditor with variant profile name
|
// Variant selected - show ProviderEditor with variant profile name
|
||||||
<ProviderEditor
|
<ProviderEditor
|
||||||
|
|||||||
Reference in New Issue
Block a user