fix(agy): enforce multi-step responsibility acknowledgement for antigravity oauth

This commit is contained in:
Tam Nhu Tran
2026-02-24 17:23:02 +07:00
parent 1dae52014b
commit 3b1271b5e4
14 changed files with 646 additions and 16 deletions
+9
View File
@@ -234,11 +234,17 @@ export function warnOAuthBanRisk(provider: CLIProxyProvider): void {
if (!isBanWarningProvider(provider) || shownBanWarnings.has(provider)) return;
shownBanWarnings.add(provider);
const isAgy = provider === 'agy';
console.error('');
console.error(warn('Account safety warning (#509 - read before continuing)'));
console.error(
' Known risk: one Google account shared by "ccs gemini" + "ccs agy" can be disabled/banned.'
);
if (isAgy) {
console.error(
' Antigravity-specific warning (#622): OAuth usage can still trigger suspension/ban patterns.'
);
}
console.error(
' This risk applies whether auth was done from CLI or from "ccs config" dashboard.'
);
@@ -249,6 +255,9 @@ export function warnOAuthBanRisk(provider: CLIProxyProvider): void {
' CCS is provided as-is and cannot take responsibility for suspension/ban/access-loss decisions.'
);
console.error(` Details: ${ISSUE_509_URL}`);
if (isAgy) {
console.error(' Antigravity details: https://github.com/kaitranntt/ccs/issues/622');
}
console.error('');
}
+212
View File
@@ -0,0 +1,212 @@
/**
* Antigravity OAuth Responsibility Gate
*
* Enforces explicit user acknowledgement for Antigravity OAuth usage.
* This is used by:
* - CLI OAuth flow (`ccs agy --auth`)
* - CLI runtime flow (`ccs agy`)
* - Dashboard auth endpoints (server-side payload validation)
*/
import { createInterface, Interface } from 'readline';
import { fail, info, ok, warn } from '../utils/ui';
export const ANTIGRAVITY_RISK_ISSUE_URL = 'https://github.com/kaitranntt/ccs/issues/622';
export const ANTIGRAVITY_PROJECT_ID_ISSUE_URL = 'https://github.com/kaitranntt/ccs/issues/619';
export const ANTIGRAVITY_ACK_VERSION = '2026-02-24-antigravity-oauth-v1';
export const ANTIGRAVITY_ACK_PHRASE = 'I ACCEPT FULL RESPONSIBILITY';
export const ANTIGRAVITY_ACCEPT_RISK_FLAGS = ['--accept-agr-risk', '--accept-antigravity-risk'];
type AgyRiskContext = 'oauth' | 'run';
export interface AntigravityRiskAcknowledgement {
version: string;
reviewedIssue622: boolean;
understandsBanRisk: boolean;
acceptsFullResponsibility: boolean;
typedPhrase: string;
}
interface ValidationResult {
valid: boolean;
error?: string;
}
interface EnsureCliRiskOptions {
context: AgyRiskContext;
acceptedByFlag?: boolean;
}
function normalizePhrase(value: string): string {
return value.trim().replace(/\s+/g, ' ').toUpperCase();
}
function isTruthyEnv(value: string | undefined): boolean {
if (!value) return false;
const normalized = value.trim().toLowerCase();
return normalized === '1' || normalized === 'true' || normalized === 'yes';
}
function askQuestion(rl: Interface, prompt: string): Promise<string | null> {
return new Promise((resolve) => {
let settled = false;
const onClose = () => {
if (!settled) {
settled = true;
resolve(null);
}
};
rl.once('close', onClose);
rl.question(prompt, (answer) => {
if (settled) return;
settled = true;
rl.removeListener('close', onClose);
resolve(answer.trim());
});
});
}
async function askYesNoStep(rl: Interface, step: string, message: string): Promise<boolean> {
while (true) {
const answer = await askQuestion(
rl,
`[?] ${step}\n ${message}\n Type YES to continue (NO to cancel): `
);
if (answer === null) return false;
const normalized = answer.toUpperCase();
if (normalized === 'YES') return true;
if (normalized === 'NO' || normalized === 'N' || normalized === '') return false;
console.error(warn('Please type YES or NO.'));
}
}
async function askResponsibilityPhrase(rl: Interface): Promise<boolean> {
for (let attempt = 0; attempt < 3; attempt++) {
const answer = await askQuestion(
rl,
`[?] Step 4/4\n Type exactly "${ANTIGRAVITY_ACK_PHRASE}": `
);
if (answer === null || answer === '') return false;
if (normalizePhrase(answer) === ANTIGRAVITY_ACK_PHRASE) {
return true;
}
console.error(warn('Phrase mismatch. Try again.'));
}
return false;
}
function printResponsibilityHeader(context: AgyRiskContext): void {
const contextLine =
context === 'oauth'
? 'You are starting Antigravity OAuth account authorization.'
: 'You are starting a live Antigravity CLI session (ccs agy).';
console.error('');
console.error('╔══════════════════════════════════════════════════════════════════════╗');
console.error('║ Antigravity Responsibility Confirmation (Mandatory) ║');
console.error('╚══════════════════════════════════════════════════════════════════════╝');
console.error(` ${contextLine}`);
console.error(' Antigravity has active ban/suspension patterns for risky OAuth usage.');
console.error(` Policy issue: ${ANTIGRAVITY_RISK_ISSUE_URL}`);
console.error(` Related account reliability issue: ${ANTIGRAVITY_PROJECT_ID_ISSUE_URL}`);
console.error('');
}
export function hasAntigravityRiskAcceptanceFlag(args: string[]): boolean {
return args.some((arg) => ANTIGRAVITY_ACCEPT_RISK_FLAGS.includes(arg));
}
export function validateAntigravityRiskAcknowledgement(payload: unknown): ValidationResult {
if (!payload || typeof payload !== 'object') {
return {
valid: false,
error: 'Antigravity OAuth requires a full responsibility acknowledgement payload.',
};
}
const data = payload as Partial<AntigravityRiskAcknowledgement>;
if (data.version !== ANTIGRAVITY_ACK_VERSION) {
return {
valid: false,
error: 'Antigravity acknowledgement version mismatch. Re-open add account and try again.',
};
}
if (!data.reviewedIssue622 || !data.understandsBanRisk || !data.acceptsFullResponsibility) {
return {
valid: false,
error: 'Complete all Antigravity responsibility checklist steps before authenticating.',
};
}
if (
typeof data.typedPhrase !== 'string' ||
normalizePhrase(data.typedPhrase) !== ANTIGRAVITY_ACK_PHRASE
) {
return {
valid: false,
error: `Type exact acknowledgement phrase: "${ANTIGRAVITY_ACK_PHRASE}".`,
};
}
return { valid: true };
}
export async function ensureCliAntigravityResponsibility(
options: EnsureCliRiskOptions
): Promise<boolean> {
if (options.acceptedByFlag || isTruthyEnv(process.env.CCS_ACCEPT_AGY_RISK)) {
return true;
}
if (!process.stdin.isTTY || !process.stderr.isTTY) {
console.error(fail('Antigravity responsibility acknowledgement required.'));
console.error(' Re-run interactively and complete the 4-step confirmation.');
console.error(' Non-interactive override: --accept-agr-risk');
return false;
}
printResponsibilityHeader(options.context);
const rl = createInterface({
input: process.stdin,
output: process.stderr,
});
try {
const step1 = await askYesNoStep(
rl,
'Step 1/4',
'I reviewed issue #622 and understand Antigravity OAuth can trigger bans/suspensions.'
);
if (!step1) return false;
const step2 = await askYesNoStep(
rl,
'Step 2/4',
'I understand this OAuth operation is my own decision and I choose to continue.'
);
if (!step2) return false;
const step3 = await askYesNoStep(
rl,
'Step 3/4',
'I accept that CCS provides no responsibility coverage for account loss, bans, or suspension.'
);
if (!step3) return false;
const step4 = await askResponsibilityPhrase(rl);
if (!step4) return false;
console.error(ok('Antigravity responsibility acknowledgement accepted for this command.'));
console.error(info('Proceeding with Antigravity flow...'));
return true;
} finally {
rl.close();
}
}
+2
View File
@@ -273,6 +273,8 @@ export interface OAuthOptions {
account?: string;
add?: boolean;
nickname?: string;
/** If true, caller explicitly accepts Antigravity OAuth risk for this command/session. */
acceptAgyRisk?: boolean;
/** Kiro auth method override (CLI + Dashboard parity). */
kiroMethod?: KiroAuthMethod;
/** If true, triggered from Web UI (enables project selection prompt) */
+20
View File
@@ -50,6 +50,7 @@ import {
warnOAuthBanRisk,
warnPossible403Ban,
} from '../account-safety';
import { ensureCliAntigravityResponsibility } from '../antigravity-responsibility';
/**
* Prompt user to add another account
@@ -429,10 +430,29 @@ export async function triggerOAuth(
const oauthConfig = getOAuthConfig(provider);
warnOAuthBanRisk(provider);
const { verbose = false, add = false, fromUI = false, noIncognito = true } = options;
const acceptAgyRisk = options.acceptAgyRisk === true;
let { nickname } = options;
const resolvedKiroMethod =
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
if (provider === 'agy') {
if (fromUI && !acceptAgyRisk) {
console.log(fail('Antigravity OAuth blocked: responsibility acknowledgement is missing.'));
return null;
}
if (!fromUI) {
const acknowledged = await ensureCliAntigravityResponsibility({
context: 'oauth',
acceptedByFlag: acceptAgyRisk,
});
if (!acknowledged) {
console.log(info('Cancelled'));
return null;
}
}
}
// Check for existing accounts
const existingAccounts = getProviderAccounts(provider);
+28
View File
@@ -72,6 +72,11 @@ import {
enforceProviderIsolation,
restoreAutoPausedAccounts,
} from '../account-safety';
import {
ensureCliAntigravityResponsibility,
hasAntigravityRiskAcceptanceFlag,
ANTIGRAVITY_ACCEPT_RISK_FLAGS,
} from '../antigravity-responsibility';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import {
buildThinkingStartupStatus,
@@ -286,6 +291,7 @@ export async function execClaudeWithCLIProxy(
const addAccount = argsWithoutProxy.includes('--add');
const showAccounts = argsWithoutProxy.includes('--accounts');
const forceImport = argsWithoutProxy.includes('--import');
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
const incognitoFlag = argsWithoutProxy.includes('--incognito');
const noIncognitoFlag = argsWithoutProxy.includes('--no-incognito');
@@ -523,6 +529,24 @@ export async function execClaudeWithCLIProxy(
log(`Using remote proxy authentication (skipping local OAuth)`);
}
if (provider === 'agy' && !forceAuth && !skipLocalAuth) {
const requiresAuthNow = providerConfig.requiresOAuth && !isAuthenticated(provider);
if (!requiresAuthNow) {
const acknowledged = await ensureCliAntigravityResponsibility({
context: 'run',
acceptedByFlag: acceptAgyRisk,
});
if (!acknowledged) {
console.error(
fail(
`Antigravity session blocked. Re-run after completing confirmation or pass ${ANTIGRAVITY_ACCEPT_RISK_FLAGS[0]}.`
)
);
process.exit(1);
}
}
}
if (providerConfig.requiresOAuth && !skipLocalAuth) {
log(`Checking authentication for ${provider}`);
@@ -536,6 +560,7 @@ export async function execClaudeWithCLIProxy(
const authSuccess = await triggerOAuth(p, {
verbose,
add: addAccount,
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
@@ -577,6 +602,7 @@ export async function execClaudeWithCLIProxy(
const authSuccess = await triggerOAuth(provider, {
verbose,
add: addAccount,
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
@@ -922,6 +948,8 @@ export async function execClaudeWithCLIProxy(
'--incognito',
'--no-incognito',
'--import',
'--accept-agr-risk',
'--accept-antigravity-risk',
'--settings',
...PROXY_CLI_FLAGS,
];
+5
View File
@@ -167,6 +167,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
'First run: Browser opens for authentication, then model selection',
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
'Safety: do not reuse one Google account across "ccs gemini" and "ccs agy" (issue #509)',
'Antigravity requires multi-step responsibility confirmation (issue #622)',
'If you want to keep Google AI access, do not continue this shared-account setup',
'CCS is as-is and does not take responsibility for account bans/access loss',
],
@@ -188,6 +189,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs <provider> --accounts', 'List all accounts'],
['ccs <provider> --use <name>', 'Switch to account'],
['ccs <provider> --config', 'Change model (agy, gemini)'],
[
'ccs agy --accept-agr-risk',
'Bypass interactive Antigravity confirmation (you accept full responsibility)',
],
[
'ccs <provider> --thinking <value>',
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
+26 -1
View File
@@ -46,6 +46,7 @@ import {
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { validateAntigravityRiskAcknowledgement } from '../../cliproxy/antigravity-responsibility';
const router = Router();
@@ -385,6 +386,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
nickname: nicknameRaw,
noIncognito: noIncognitoBody,
kiroMethod: kiroMethodRaw,
riskAcknowledgement,
} = req.body;
// Trim nickname for consistency with CLI (oauth-handler.ts trims input)
const nickname = typeof nicknameRaw === 'string' ? nicknameRaw.trim() : nicknameRaw;
@@ -404,6 +406,17 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
return;
}
if (provider === 'agy') {
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
if (!validation.valid) {
res.status(400).json({
error: validation.error,
code: 'AGY_RISK_ACK_REQUIRED',
});
return;
}
}
// For kiro/ghcp: nickname is required
if (PROVIDERS_WITHOUT_EMAIL.includes(provider as CLIProxyProvider)) {
if (!nickname) {
@@ -451,6 +464,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
add: true, // Always add mode from UI
headless: false, // Force interactive mode
nickname: nickname || undefined,
acceptAgyRisk: provider === 'agy',
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
fromUI: true, // Enable project selection prompt in UI
noIncognito, // Kiro: use normal browser if enabled
@@ -596,7 +610,7 @@ router.post('/kiro/import', async (_req: Request, res: Response): Promise<void>
*/
router.post('/:provider/start-url', async (req: Request, res: Response): Promise<void> => {
const { provider } = req.params;
const { kiroMethod: kiroMethodRaw } = req.body ?? {};
const { kiroMethod: kiroMethodRaw, riskAcknowledgement } = req.body ?? {};
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
// Check remote mode
@@ -620,6 +634,17 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
return;
}
if (provider === 'agy') {
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
if (!validation.valid) {
res.status(400).json({
error: validation.error,
code: 'AGY_RISK_ACK_REQUIRED',
});
return;
}
}
const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider, {
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
});
@@ -0,0 +1,78 @@
import { describe, expect, it } from 'bun:test';
import {
ANTIGRAVITY_ACK_PHRASE,
ANTIGRAVITY_ACK_VERSION,
hasAntigravityRiskAcceptanceFlag,
validateAntigravityRiskAcknowledgement,
} from '../../../src/cliproxy/antigravity-responsibility';
describe('antigravity-responsibility', () => {
it('accepts a complete acknowledgement payload', () => {
const result = validateAntigravityRiskAcknowledgement({
version: ANTIGRAVITY_ACK_VERSION,
reviewedIssue622: true,
understandsBanRisk: true,
acceptsFullResponsibility: true,
typedPhrase: ANTIGRAVITY_ACK_PHRASE,
});
expect(result.valid).toBeTrue();
});
it('accepts phrase with extra spacing and lowercase', () => {
const result = validateAntigravityRiskAcknowledgement({
version: ANTIGRAVITY_ACK_VERSION,
reviewedIssue622: true,
understandsBanRisk: true,
acceptsFullResponsibility: true,
typedPhrase: ' i accept full responsibility ',
});
expect(result.valid).toBeTrue();
});
it('rejects payload when checklist steps are not fully completed', () => {
const result = validateAntigravityRiskAcknowledgement({
version: ANTIGRAVITY_ACK_VERSION,
reviewedIssue622: true,
understandsBanRisk: false,
acceptsFullResponsibility: true,
typedPhrase: ANTIGRAVITY_ACK_PHRASE,
});
expect(result.valid).toBeFalse();
expect(result.error).toContain('checklist');
});
it('rejects payload when version is outdated', () => {
const result = validateAntigravityRiskAcknowledgement({
version: 'older-version',
reviewedIssue622: true,
understandsBanRisk: true,
acceptsFullResponsibility: true,
typedPhrase: ANTIGRAVITY_ACK_PHRASE,
});
expect(result.valid).toBeFalse();
expect(result.error).toContain('version');
});
it('rejects payload when phrase does not match', () => {
const result = validateAntigravityRiskAcknowledgement({
version: ANTIGRAVITY_ACK_VERSION,
reviewedIssue622: true,
understandsBanRisk: true,
acceptsFullResponsibility: true,
typedPhrase: 'I AGREE',
});
expect(result.valid).toBeFalse();
expect(result.error).toContain('phrase');
});
it('detects explicit antigravity acceptance flags', () => {
expect(hasAntigravityRiskAcceptanceFlag(['--accept-agr-risk'])).toBeTrue();
expect(hasAntigravityRiskAcceptanceFlag(['--accept-antigravity-risk'])).toBeTrue();
expect(hasAntigravityRiskAcceptanceFlag(['--auth'])).toBeFalse();
});
});
@@ -6,6 +6,7 @@ import { cn } from '@/lib/utils';
interface AccountSafetyWarningCardProps {
className?: string;
provider?: 'gemini' | 'agy';
showAcknowledgement?: boolean;
acknowledged?: boolean;
onAcknowledgedChange?: (value: boolean) => void;
@@ -14,11 +15,39 @@ interface AccountSafetyWarningCardProps {
export function AccountSafetyWarningCard({
className,
provider = 'gemini',
showAcknowledgement = false,
acknowledged = false,
onAcknowledgedChange,
disabled = false,
}: AccountSafetyWarningCardProps) {
const isAgy = provider === 'agy';
const title = isAgy ? 'Antigravity OAuth Risk' : 'Account Safety Warning';
const subtitle = isAgy
? 'Issue #622 · Third-party OAuth ban risk'
: 'Issue #509 · Shared Gemini + AGY account risk';
const firstLine = isAgy ? (
<>
Antigravity OAuth currently has active ban/suspension patterns. Complete the responsibility
steps before running auth or starting <code className="font-mono">ccs agy</code>.
</>
) : (
<>
Using one Google account for both <code className="font-mono">ccs gemini</code> and{' '}
<code className="font-mono">ccs agy</code> can trigger account disable/ban.
</>
);
const secondLine = isAgy ? (
<>If you want to keep this account, do not continue unless you accept full responsibility.</>
) : (
<>If you want to keep Google AI access, do not continue this shared-account setup.</>
);
const issueUrl = isAgy
? 'https://github.com/kaitranntt/ccs/issues/622'
: 'https://github.com/kaitranntt/ccs/issues/509';
const issueLabel = isAgy ? 'Read issue #622' : 'Read issue #509';
return (
<section
role="alert"
@@ -36,10 +65,8 @@ export function AccountSafetyWarningCard({
<AlertTriangle className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-semibold leading-5">Account Safety Warning</p>
<p className="text-xs text-muted-foreground">
Issue #509 · Shared Gemini + AGY account risk
</p>
<p className="text-sm font-semibold leading-5">{title}</p>
<p className="text-xs text-muted-foreground">{subtitle}</p>
</div>
</div>
<Badge
@@ -51,13 +78,8 @@ export function AccountSafetyWarningCard({
</div>
<div className="space-y-2 text-sm leading-relaxed">
<p>
Using one Google account for both <code className="font-mono">ccs gemini</code> and{' '}
<code className="font-mono">ccs agy</code> can trigger account disable/ban.
</p>
<p className="font-medium text-amber-900 dark:text-amber-200">
If you want to keep Google AI access, do not continue this shared-account setup.
</p>
<p>{firstLine}</p>
<p className="font-medium text-amber-900 dark:text-amber-200">{secondLine}</p>
<p className="text-xs text-muted-foreground">
CCS is provided as-is and does not take responsibility for suspension, bans, or access
loss from upstream providers.
@@ -66,12 +88,12 @@ export function AccountSafetyWarningCard({
<div className="flex flex-wrap items-center gap-2">
<a
href="https://github.com/kaitranntt/ccs/issues/509"
href={issueUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-800 transition-colors hover:bg-amber-500/15 dark:text-amber-200"
>
Read issue #509
{issueLabel}
<ExternalLink className="h-3.5 w-3.5" />
</a>
<span className="rounded-md border border-border/70 bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground">
@@ -30,6 +30,12 @@ import { useKiroImport } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { applyDefaultPreset } from '@/lib/preset-utils';
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
import { AntigravityResponsibilityChecklist } from '@/components/account/antigravity-responsibility-checklist';
import {
ANTIGRAVITY_ACK_VERSION,
DEFAULT_ANTIGRAVITY_RISK_CHECKLIST,
isAntigravityRiskChecklistComplete,
} from '@/components/account/antigravity-responsibility-constants';
import {
DEFAULT_KIRO_AUTH_METHOD,
getKiroAuthMethodOption,
@@ -61,13 +67,16 @@ export function AddAccountDialog({
const [copied, setCopied] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
const [acknowledgedRisk, setAcknowledgedRisk] = useState(false);
const [agyRiskChecklist, setAgyRiskChecklist] = useState(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
const wasAuthenticatingRef = useRef(false);
const authFlow = useCliproxyAuthFlow();
const kiroImportMutation = useKiroImport();
const isKiro = provider === 'kiro';
const requiresSafetyAcknowledgement = provider === 'gemini' || provider === 'agy';
const requiresSafetyAcknowledgement = provider === 'gemini';
const requiresAgyResponsibilityFlow = provider === 'agy';
const isAgyRiskChecklistComplete = isAntigravityRiskChecklistComplete(agyRiskChecklist);
const defaultDeviceCode = isDeviceCodeProvider(provider);
const requiresNickname = isNicknameRequiredProvider(provider);
const kiroMethodOption = getKiroAuthMethodOption(kiroAuthMethod);
@@ -82,6 +91,7 @@ export function AddAccountDialog({
setCopied(false);
setLocalError(null);
setAcknowledgedRisk(false);
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
wasAuthenticatingRef.current = false;
onClose();
@@ -90,6 +100,7 @@ export function AddAccountDialog({
useEffect(() => {
if (open) {
setAcknowledgedRisk(false);
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
setLocalError(null);
}
}, [provider, open]);
@@ -142,6 +153,12 @@ export function AddAccountDialog({
* - Authorization code providers use /start-url and polling.
*/
const handleAuthenticate = () => {
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
setLocalError(
'Complete all Antigravity responsibility steps before authenticating this provider.'
);
return;
}
if (requiresSafetyAcknowledgement && !acknowledgedRisk) {
setLocalError(
'Please acknowledge the account safety warning before authenticating this provider.'
@@ -159,6 +176,15 @@ export function AddAccountDialog({
kiroMethod: isKiro ? kiroAuthMethod : undefined,
flowType: isKiro ? kiroMethodOption.flowType : undefined,
startEndpoint: isKiro ? kiroMethodOption.startEndpoint : undefined,
riskAcknowledgement: requiresAgyResponsibilityFlow
? {
version: ANTIGRAVITY_ACK_VERSION,
reviewedIssue622: agyRiskChecklist.reviewedIssue622,
understandsBanRisk: agyRiskChecklist.understandsBanRisk,
acceptsFullResponsibility: agyRiskChecklist.acceptsFullResponsibility,
typedPhrase: agyRiskChecklist.typedPhrase,
}
: undefined,
});
};
@@ -204,8 +230,20 @@ export function AddAccountDialog({
</DialogHeader>
<div className="space-y-4 py-4">
{requiresAgyResponsibilityFlow && !showAuthUI && (
<AntigravityResponsibilityChecklist
value={agyRiskChecklist}
onChange={(value) => {
setAgyRiskChecklist(value);
setLocalError(null);
}}
disabled={isPending}
/>
)}
{requiresSafetyAcknowledgement && !showAuthUI && (
<AccountSafetyWarningCard
provider="gemini"
showAcknowledgement
acknowledged={acknowledgedRisk}
onAcknowledgedChange={(value) => {
@@ -406,6 +444,7 @@ export function AddAccountDialog({
disabled={
isPending ||
(requiresNickname && !nicknameTrimmed) ||
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
(requiresSafetyAcknowledgement && !acknowledgedRisk)
}
>
@@ -0,0 +1,154 @@
import { AlertTriangle, ExternalLink, ShieldAlert } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
import {
ANTIGRAVITY_ACK_PHRASE,
AntigravityRiskChecklistValue,
} from '@/components/account/antigravity-responsibility-constants';
interface AntigravityResponsibilityChecklistProps {
className?: string;
value: AntigravityRiskChecklistValue;
onChange: (value: AntigravityRiskChecklistValue) => void;
disabled?: boolean;
}
export function AntigravityResponsibilityChecklist({
className,
value,
onChange,
disabled = false,
}: AntigravityResponsibilityChecklistProps) {
const completedSteps = [
value.reviewedIssue622,
value.understandsBanRisk,
value.acceptsFullResponsibility,
value.typedPhrase.trim().replace(/\s+/g, ' ').toUpperCase() === ANTIGRAVITY_ACK_PHRASE,
].filter(Boolean).length;
const progressValue = (completedSteps / 4) * 100;
const setValue = (next: Partial<AntigravityRiskChecklistValue>) => {
onChange({ ...value, ...next });
};
return (
<section
role="alert"
className={cn(
'relative overflow-hidden rounded-xl border border-rose-500/35 bg-gradient-to-br from-rose-50 via-background to-amber-50/70 p-4 shadow-sm dark:from-rose-950/20 dark:to-amber-950/20',
className
)}
>
<div className="absolute inset-x-0 top-0 h-0.5 bg-gradient-to-r from-rose-500 via-orange-500 to-amber-500" />
<div className="space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-2.5">
<div className="mt-0.5 inline-flex h-7 w-7 items-center justify-center rounded-md bg-rose-500/15 text-rose-700 dark:text-rose-300">
<AlertTriangle className="h-4 w-4" />
</div>
<div className="space-y-1">
<p className="text-sm font-semibold leading-5">Antigravity OAuth Responsibility</p>
<p className="text-xs text-muted-foreground">
Complete all 4 steps before you can authenticate.
</p>
</div>
</div>
<Badge variant="outline" className="border-rose-500/40 text-rose-700 dark:text-rose-300">
Mandatory
</Badge>
</div>
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Completion</span>
<span>{completedSteps}/4 steps</span>
</div>
<Progress value={progressValue} className="h-2" />
</div>
<div className="space-y-3 rounded-lg border border-rose-500/20 bg-rose-500/5 p-3">
<div className="flex items-start gap-2">
<Checkbox
id="agy-step-reviewed-issue"
checked={value.reviewedIssue622}
onCheckedChange={(checked) => setValue({ reviewedIssue622: Boolean(checked) })}
disabled={disabled}
/>
<Label htmlFor="agy-step-reviewed-issue" className="text-xs leading-5">
Step 1: I reviewed issue #622 and understand Antigravity OAuth can trigger account
bans/suspensions.
</Label>
</div>
<div className="flex items-start gap-2">
<Checkbox
id="agy-step-understands-risk"
checked={value.understandsBanRisk}
onCheckedChange={(checked) => setValue({ understandsBanRisk: Boolean(checked) })}
disabled={disabled}
/>
<Label htmlFor="agy-step-understands-risk" className="text-xs leading-5">
Step 2: I understand this OAuth action is my own decision and I accept the upstream
risk.
</Label>
</div>
<div className="flex items-start gap-2">
<Checkbox
id="agy-step-accept-responsibility"
checked={value.acceptsFullResponsibility}
onCheckedChange={(checked) =>
setValue({ acceptsFullResponsibility: Boolean(checked) })
}
disabled={disabled}
/>
<Label htmlFor="agy-step-accept-responsibility" className="text-xs leading-5">
Step 3: I accept full responsibility. CCS is not liable for suspension, bans, or
access loss.
</Label>
</div>
</div>
<div className="space-y-2 rounded-lg border border-amber-500/25 bg-amber-500/5 p-3">
<div className="flex items-center gap-1.5 text-xs font-medium text-amber-800 dark:text-amber-200">
<ShieldAlert className="h-3.5 w-3.5" />
Step 4: Type exact phrase to continue
</div>
<Input
value={value.typedPhrase}
onChange={(e) => setValue({ typedPhrase: e.target.value })}
placeholder={ANTIGRAVITY_ACK_PHRASE}
disabled={disabled}
className="font-mono text-xs"
/>
</div>
<div className="flex flex-wrap items-center gap-2 text-xs">
<a
href="https://github.com/kaitranntt/ccs/issues/622"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md border border-rose-500/30 bg-rose-500/10 px-2.5 py-1 font-medium text-rose-800 transition-colors hover:bg-rose-500/15 dark:text-rose-200"
>
Read issue #622
<ExternalLink className="h-3.5 w-3.5" />
</a>
<a
href="https://github.com/kaitranntt/ccs/issues/619"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-1 font-medium text-amber-800 transition-colors hover:bg-amber-500/15 dark:text-amber-200"
>
Related issue #619
<ExternalLink className="h-3.5 w-3.5" />
</a>
</div>
</div>
</section>
);
}
@@ -0,0 +1,25 @@
export const ANTIGRAVITY_ACK_VERSION = '2026-02-24-antigravity-oauth-v1';
export const ANTIGRAVITY_ACK_PHRASE = 'I ACCEPT FULL RESPONSIBILITY';
export interface AntigravityRiskChecklistValue {
reviewedIssue622: boolean;
understandsBanRisk: boolean;
acceptsFullResponsibility: boolean;
typedPhrase: string;
}
export const DEFAULT_ANTIGRAVITY_RISK_CHECKLIST: AntigravityRiskChecklistValue = {
reviewedIssue622: false,
understandsBanRisk: false,
acceptsFullResponsibility: false,
typedPhrase: '',
};
export function isAntigravityRiskChecklistComplete(value: AntigravityRiskChecklistValue): boolean {
return (
value.reviewedIssue622 &&
value.understandsBanRisk &&
value.acceptsFullResponsibility &&
value.typedPhrase.trim().replace(/\s+/g, ' ').toUpperCase() === ANTIGRAVITY_ACK_PHRASE
);
}
+8
View File
@@ -28,6 +28,13 @@ interface StartAuthOptions {
kiroMethod?: string;
flowType?: 'authorization_code' | 'device_code';
startEndpoint?: 'start' | 'start-url';
riskAcknowledgement?: {
version: string;
reviewedIssue622: boolean;
understandsBanRisk: boolean;
acceptsFullResponsibility: boolean;
typedPhrase: string;
};
}
/** Polling interval for OAuth status check (3 seconds) */
@@ -175,6 +182,7 @@ export function useCliproxyAuthFlow() {
const payload = {
nickname: options?.nickname,
kiroMethod: options?.kiroMethod,
riskAcknowledgement: options?.riskAcknowledgement,
};
setState({
+4 -1
View File
@@ -248,6 +248,7 @@ export function CliproxyPage() {
.toLowerCase()
.trim();
const showAccountSafetyWarning = warningProvider === 'gemini' || warningProvider === 'agy';
const warningProviderType = warningProvider === 'agy' ? 'agy' : 'gemini';
const handleRefresh = () => {
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
@@ -394,7 +395,9 @@ export function CliproxyPage() {
{/* Right Panel */}
<div className="flex-1 flex flex-col min-w-0 bg-background">
{showAccountSafetyWarning && <AccountSafetyWarningCard className="mx-4 mt-4" />}
{showAccountSafetyWarning && (
<AccountSafetyWarningCard provider={warningProviderType} className="mx-4 mt-4" />
)}
{selectedVariantData && parentAuthForVariant ? (
// Variant selected - show ProviderEditor with variant profile name