From 3b1271b5e4bd48e65d599323c07eebdffd7c8995 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 24 Feb 2026 17:23:02 +0700 Subject: [PATCH] fix(agy): enforce multi-step responsibility acknowledgement for antigravity oauth --- src/cliproxy/account-safety.ts | 9 + src/cliproxy/antigravity-responsibility.ts | 212 ++++++++++++++++++ src/cliproxy/auth/auth-types.ts | 2 + src/cliproxy/auth/oauth-handler.ts | 20 ++ src/cliproxy/executor/index.ts | 28 +++ src/commands/help-command.ts | 5 + src/web-server/routes/cliproxy-auth-routes.ts | 27 ++- .../antigravity-responsibility.test.ts | 78 +++++++ .../account/account-safety-warning-card.tsx | 48 ++-- .../components/account/add-account-dialog.tsx | 41 +++- .../antigravity-responsibility-checklist.tsx | 154 +++++++++++++ .../antigravity-responsibility-constants.ts | 25 +++ ui/src/hooks/use-cliproxy-auth-flow.ts | 8 + ui/src/pages/cliproxy.tsx | 5 +- 14 files changed, 646 insertions(+), 16 deletions(-) create mode 100644 src/cliproxy/antigravity-responsibility.ts create mode 100644 tests/unit/cliproxy/antigravity-responsibility.test.ts create mode 100644 ui/src/components/account/antigravity-responsibility-checklist.tsx create mode 100644 ui/src/components/account/antigravity-responsibility-constants.ts diff --git a/src/cliproxy/account-safety.ts b/src/cliproxy/account-safety.ts index 6c7e049d..8f981c33 100644 --- a/src/cliproxy/account-safety.ts +++ b/src/cliproxy/account-safety.ts @@ -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(''); } diff --git a/src/cliproxy/antigravity-responsibility.ts b/src/cliproxy/antigravity-responsibility.ts new file mode 100644 index 00000000..0197e651 --- /dev/null +++ b/src/cliproxy/antigravity-responsibility.ts @@ -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 { + 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 { + 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 { + 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; + + 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 { + 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(); + } +} diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index 92080547..9d1cf401 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -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) */ diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index f05316d2..f4a2d3b2 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -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); diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 220ccdd2..c3f2f3bb 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -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, ]; diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index c7b52ae7..fd80db89 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -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 --accounts', 'List all accounts'], ['ccs --use ', 'Switch to account'], ['ccs --config', 'Change model (agy, gemini)'], + [ + 'ccs agy --accept-agr-risk', + 'Bypass interactive Antigravity confirmation (you accept full responsibility)', + ], [ 'ccs --thinking ', 'Set thinking budget (low/medium/high/xhigh/auto/off or number)', diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index b07da8e2..14026eca 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -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 */ router.post('/:provider/start-url', async (req: Request, res: Response): Promise => { 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, }); diff --git a/tests/unit/cliproxy/antigravity-responsibility.test.ts b/tests/unit/cliproxy/antigravity-responsibility.test.ts new file mode 100644 index 00000000..0ae0ca7c --- /dev/null +++ b/tests/unit/cliproxy/antigravity-responsibility.test.ts @@ -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(); + }); +}); diff --git a/ui/src/components/account/account-safety-warning-card.tsx b/ui/src/components/account/account-safety-warning-card.tsx index a2e00ae0..7d167cfd 100644 --- a/ui/src/components/account/account-safety-warning-card.tsx +++ b/ui/src/components/account/account-safety-warning-card.tsx @@ -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 ccs agy. + + ) : ( + <> + Using one Google account for both ccs gemini and{' '} + ccs agy 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 (
-

Account Safety Warning

-

- Issue #509 · Shared Gemini + AGY account risk -

+

{title}

+

{subtitle}

-

- Using one Google account for both ccs gemini and{' '} - ccs agy can trigger account disable/ban. -

-

- If you want to keep Google AI access, do not continue this shared-account setup. -

+

{firstLine}

+

{secondLine}

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({

- Read issue #509 + {issueLabel} diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx index 8a203a9b..41041441 100644 --- a/ui/src/components/account/add-account-dialog.tsx +++ b/ui/src/components/account/add-account-dialog.tsx @@ -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(null); const [acknowledgedRisk, setAcknowledgedRisk] = useState(false); + const [agyRiskChecklist, setAgyRiskChecklist] = useState(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST); const [kiroAuthMethod, setKiroAuthMethod] = useState(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({
+ {requiresAgyResponsibilityFlow && !showAuthUI && ( + { + setAgyRiskChecklist(value); + setLocalError(null); + }} + disabled={isPending} + /> + )} + {requiresSafetyAcknowledgement && !showAuthUI && ( { @@ -406,6 +444,7 @@ export function AddAccountDialog({ disabled={ isPending || (requiresNickname && !nicknameTrimmed) || + (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) || (requiresSafetyAcknowledgement && !acknowledgedRisk) } > diff --git a/ui/src/components/account/antigravity-responsibility-checklist.tsx b/ui/src/components/account/antigravity-responsibility-checklist.tsx new file mode 100644 index 00000000..6940c246 --- /dev/null +++ b/ui/src/components/account/antigravity-responsibility-checklist.tsx @@ -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) => { + onChange({ ...value, ...next }); + }; + + return ( +
+
+ +
+
+
+
+ +
+
+

Antigravity OAuth Responsibility

+

+ Complete all 4 steps before you can authenticate. +

+
+
+ + Mandatory + +
+ +
+
+ Completion + {completedSteps}/4 steps +
+ +
+ +
+
+ setValue({ reviewedIssue622: Boolean(checked) })} + disabled={disabled} + /> + +
+ +
+ setValue({ understandsBanRisk: Boolean(checked) })} + disabled={disabled} + /> + +
+ +
+ + setValue({ acceptsFullResponsibility: Boolean(checked) }) + } + disabled={disabled} + /> + +
+
+ +
+
+ + Step 4: Type exact phrase to continue +
+ setValue({ typedPhrase: e.target.value })} + placeholder={ANTIGRAVITY_ACK_PHRASE} + disabled={disabled} + className="font-mono text-xs" + /> +
+ + +
+
+ ); +} diff --git a/ui/src/components/account/antigravity-responsibility-constants.ts b/ui/src/components/account/antigravity-responsibility-constants.ts new file mode 100644 index 00000000..4ecda80b --- /dev/null +++ b/ui/src/components/account/antigravity-responsibility-constants.ts @@ -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 + ); +} diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index a1f8f954..4f8bb8f9 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -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({ diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 5d12c178..d340a142 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -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 */}
- {showAccountSafetyWarning && } + {showAccountSafetyWarning && ( + + )} {selectedVariantData && parentAuthForVariant ? ( // Variant selected - show ProviderEditor with variant profile name