mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 22:15:33 +00:00
fix(agy): enforce multi-step responsibility acknowledgement for antigravity oauth
This commit is contained in:
1 parent
1dae52014b
commit
3b1271b5e4
14 files changed
+646
-16
No files matched your search
@@ -234,11 +234,17 @@ export function warnOAuthBanRisk(provider: CLIProxyProvider): void {
|
|||||||
if (!isBanWarningProvider(provider) || shownBanWarnings.has(provider)) return;
|
if (!isBanWarningProvider(provider) || shownBanWarnings.has(provider)) return;
|
||||||
|
|
||||||
shownBanWarnings.add(provider);
|
shownBanWarnings.add(provider);
|
||||||
|
const isAgy = provider === 'agy';
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(warn('Account safety warning (#509 - read before continuing)'));
|
console.error(warn('Account safety warning (#509 - read before continuing)'));
|
||||||
console.error(
|
console.error(
|
||||||
' Known risk: one Google account shared by "ccs gemini" + "ccs agy" can be disabled/banned.'
|
' 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(
|
console.error(
|
||||||
' This risk applies whether auth was done from CLI or from "ccs config" dashboard.'
|
' 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.'
|
' CCS is provided as-is and cannot take responsibility for suspension/ban/access-loss decisions.'
|
||||||
);
|
);
|
||||||
console.error(` Details: ${ISSUE_509_URL}`);
|
console.error(` Details: ${ISSUE_509_URL}`);
|
||||||
|
if (isAgy) {
|
||||||
|
console.error(' Antigravity details: https://github.com/kaitranntt/ccs/issues/622');
|
||||||
|
}
|
||||||
console.error('');
|
console.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -273,6 +273,8 @@ export interface OAuthOptions {
|
|||||||
account?: string;
|
account?: string;
|
||||||
add?: boolean;
|
add?: boolean;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
|
/** If true, caller explicitly accepts Antigravity OAuth risk for this command/session. */
|
||||||
|
acceptAgyRisk?: boolean;
|
||||||
/** Kiro auth method override (CLI + Dashboard parity). */
|
/** Kiro auth method override (CLI + Dashboard parity). */
|
||||||
kiroMethod?: KiroAuthMethod;
|
kiroMethod?: KiroAuthMethod;
|
||||||
/** If true, triggered from Web UI (enables project selection prompt) */
|
/** If true, triggered from Web UI (enables project selection prompt) */
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
warnOAuthBanRisk,
|
warnOAuthBanRisk,
|
||||||
warnPossible403Ban,
|
warnPossible403Ban,
|
||||||
} from '../account-safety';
|
} from '../account-safety';
|
||||||
|
import { ensureCliAntigravityResponsibility } from '../antigravity-responsibility';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt user to add another account
|
* Prompt user to add another account
|
||||||
@@ -429,10 +430,29 @@ export async function triggerOAuth(
|
|||||||
const oauthConfig = getOAuthConfig(provider);
|
const oauthConfig = getOAuthConfig(provider);
|
||||||
warnOAuthBanRisk(provider);
|
warnOAuthBanRisk(provider);
|
||||||
const { verbose = false, add = false, fromUI = false, noIncognito = true } = options;
|
const { verbose = false, add = false, fromUI = false, noIncognito = true } = options;
|
||||||
|
const acceptAgyRisk = options.acceptAgyRisk === true;
|
||||||
let { nickname } = options;
|
let { nickname } = options;
|
||||||
const resolvedKiroMethod =
|
const resolvedKiroMethod =
|
||||||
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
|
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
|
// Check for existing accounts
|
||||||
const existingAccounts = getProviderAccounts(provider);
|
const existingAccounts = getProviderAccounts(provider);
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ import {
|
|||||||
enforceProviderIsolation,
|
enforceProviderIsolation,
|
||||||
restoreAutoPausedAccounts,
|
restoreAutoPausedAccounts,
|
||||||
} from '../account-safety';
|
} from '../account-safety';
|
||||||
|
import {
|
||||||
|
ensureCliAntigravityResponsibility,
|
||||||
|
hasAntigravityRiskAcceptanceFlag,
|
||||||
|
ANTIGRAVITY_ACCEPT_RISK_FLAGS,
|
||||||
|
} from '../antigravity-responsibility';
|
||||||
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
|
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
|
||||||
import {
|
import {
|
||||||
buildThinkingStartupStatus,
|
buildThinkingStartupStatus,
|
||||||
@@ -286,6 +291,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const addAccount = argsWithoutProxy.includes('--add');
|
const addAccount = argsWithoutProxy.includes('--add');
|
||||||
const showAccounts = argsWithoutProxy.includes('--accounts');
|
const showAccounts = argsWithoutProxy.includes('--accounts');
|
||||||
const forceImport = argsWithoutProxy.includes('--import');
|
const forceImport = argsWithoutProxy.includes('--import');
|
||||||
|
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
|
||||||
|
|
||||||
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
const incognitoFlag = argsWithoutProxy.includes('--incognito');
|
||||||
const noIncognitoFlag = argsWithoutProxy.includes('--no-incognito');
|
const noIncognitoFlag = argsWithoutProxy.includes('--no-incognito');
|
||||||
@@ -523,6 +529,24 @@ export async function execClaudeWithCLIProxy(
|
|||||||
log(`Using remote proxy authentication (skipping local OAuth)`);
|
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) {
|
if (providerConfig.requiresOAuth && !skipLocalAuth) {
|
||||||
log(`Checking authentication for ${provider}`);
|
log(`Checking authentication for ${provider}`);
|
||||||
|
|
||||||
@@ -536,6 +560,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const authSuccess = await triggerOAuth(p, {
|
const authSuccess = await triggerOAuth(p, {
|
||||||
verbose,
|
verbose,
|
||||||
add: addAccount,
|
add: addAccount,
|
||||||
|
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
||||||
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
|
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
|
||||||
...(forceHeadless ? { headless: true } : {}),
|
...(forceHeadless ? { headless: true } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
@@ -577,6 +602,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const authSuccess = await triggerOAuth(provider, {
|
const authSuccess = await triggerOAuth(provider, {
|
||||||
verbose,
|
verbose,
|
||||||
add: addAccount,
|
add: addAccount,
|
||||||
|
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
||||||
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
||||||
...(forceHeadless ? { headless: true } : {}),
|
...(forceHeadless ? { headless: true } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
@@ -922,6 +948,8 @@ export async function execClaudeWithCLIProxy(
|
|||||||
'--incognito',
|
'--incognito',
|
||||||
'--no-incognito',
|
'--no-incognito',
|
||||||
'--import',
|
'--import',
|
||||||
|
'--accept-agr-risk',
|
||||||
|
'--accept-antigravity-risk',
|
||||||
'--settings',
|
'--settings',
|
||||||
...PROXY_CLI_FLAGS,
|
...PROXY_CLI_FLAGS,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|||||||
'First run: Browser opens for authentication, then model selection',
|
'First run: Browser opens for authentication, then model selection',
|
||||||
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
|
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
|
||||||
'Safety: do not reuse one Google account across "ccs gemini" and "ccs agy" (issue #509)',
|
'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',
|
'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',
|
'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> --accounts', 'List all accounts'],
|
||||||
['ccs <provider> --use <name>', 'Switch to account'],
|
['ccs <provider> --use <name>', 'Switch to account'],
|
||||||
['ccs <provider> --config', 'Change model (agy, gemini)'],
|
['ccs <provider> --config', 'Change model (agy, gemini)'],
|
||||||
|
[
|
||||||
|
'ccs agy --accept-agr-risk',
|
||||||
|
'Bypass interactive Antigravity confirmation (you accept full responsibility)',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'ccs <provider> --thinking <value>',
|
'ccs <provider> --thinking <value>',
|
||||||
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
|
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
|
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
|
||||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||||
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
||||||
|
import { validateAntigravityRiskAcknowledgement } from '../../cliproxy/antigravity-responsibility';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -385,6 +386,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
nickname: nicknameRaw,
|
nickname: nicknameRaw,
|
||||||
noIncognito: noIncognitoBody,
|
noIncognito: noIncognitoBody,
|
||||||
kiroMethod: kiroMethodRaw,
|
kiroMethod: kiroMethodRaw,
|
||||||
|
riskAcknowledgement,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
// Trim nickname for consistency with CLI (oauth-handler.ts trims input)
|
// Trim nickname for consistency with CLI (oauth-handler.ts trims input)
|
||||||
const nickname = typeof nicknameRaw === 'string' ? nicknameRaw.trim() : nicknameRaw;
|
const nickname = typeof nicknameRaw === 'string' ? nicknameRaw.trim() : nicknameRaw;
|
||||||
@@ -404,6 +406,17 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
return;
|
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
|
// For kiro/ghcp: nickname is required
|
||||||
if (PROVIDERS_WITHOUT_EMAIL.includes(provider as CLIProxyProvider)) {
|
if (PROVIDERS_WITHOUT_EMAIL.includes(provider as CLIProxyProvider)) {
|
||||||
if (!nickname) {
|
if (!nickname) {
|
||||||
@@ -451,6 +464,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
add: true, // Always add mode from UI
|
add: true, // Always add mode from UI
|
||||||
headless: false, // Force interactive mode
|
headless: false, // Force interactive mode
|
||||||
nickname: nickname || undefined,
|
nickname: nickname || undefined,
|
||||||
|
acceptAgyRisk: provider === 'agy',
|
||||||
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
|
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
|
||||||
fromUI: true, // Enable project selection prompt in UI
|
fromUI: true, // Enable project selection prompt in UI
|
||||||
noIncognito, // Kiro: use normal browser if enabled
|
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> => {
|
router.post('/:provider/start-url', async (req: Request, res: Response): Promise<void> => {
|
||||||
const { provider } = req.params;
|
const { provider } = req.params;
|
||||||
const { kiroMethod: kiroMethodRaw } = req.body ?? {};
|
const { kiroMethod: kiroMethodRaw, riskAcknowledgement } = req.body ?? {};
|
||||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||||
|
|
||||||
// Check remote mode
|
// Check remote mode
|
||||||
@@ -620,6 +634,17 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
return;
|
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, {
|
const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider, {
|
||||||
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
|
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 {
|
interface AccountSafetyWarningCardProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
provider?: 'gemini' | 'agy';
|
||||||
showAcknowledgement?: boolean;
|
showAcknowledgement?: boolean;
|
||||||
acknowledged?: boolean;
|
acknowledged?: boolean;
|
||||||
onAcknowledgedChange?: (value: boolean) => void;
|
onAcknowledgedChange?: (value: boolean) => void;
|
||||||
@@ -14,11 +15,39 @@ interface AccountSafetyWarningCardProps {
|
|||||||
|
|
||||||
export function AccountSafetyWarningCard({
|
export function AccountSafetyWarningCard({
|
||||||
className,
|
className,
|
||||||
|
provider = 'gemini',
|
||||||
showAcknowledgement = false,
|
showAcknowledgement = false,
|
||||||
acknowledged = false,
|
acknowledged = false,
|
||||||
onAcknowledgedChange,
|
onAcknowledgedChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: AccountSafetyWarningCardProps) {
|
}: 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 (
|
return (
|
||||||
<section
|
<section
|
||||||
role="alert"
|
role="alert"
|
||||||
@@ -36,10 +65,8 @@ export function AccountSafetyWarningCard({
|
|||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold leading-5">Account Safety Warning</p>
|
<p className="text-sm font-semibold leading-5">{title}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">{subtitle}</p>
|
||||||
Issue #509 · Shared Gemini + AGY account risk
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -51,13 +78,8 @@ export function AccountSafetyWarningCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 text-sm leading-relaxed">
|
<div className="space-y-2 text-sm leading-relaxed">
|
||||||
<p>
|
<p>{firstLine}</p>
|
||||||
Using one Google account for both <code className="font-mono">ccs gemini</code> and{' '}
|
<p className="font-medium text-amber-900 dark:text-amber-200">{secondLine}</p>
|
||||||
<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 className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
CCS is provided as-is and does not take responsibility for suspension, bans, or access
|
CCS is provided as-is and does not take responsibility for suspension, bans, or access
|
||||||
loss from upstream providers.
|
loss from upstream providers.
|
||||||
@@ -66,12 +88,12 @@ export function AccountSafetyWarningCard({
|
|||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/kaitranntt/ccs/issues/509"
|
href={issueUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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"
|
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" />
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
</a>
|
</a>
|
||||||
<span className="rounded-md border border-border/70 bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground">
|
<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 { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
|
||||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
import { applyDefaultPreset } from '@/lib/preset-utils';
|
||||||
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
|
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 {
|
import {
|
||||||
DEFAULT_KIRO_AUTH_METHOD,
|
DEFAULT_KIRO_AUTH_METHOD,
|
||||||
getKiroAuthMethodOption,
|
getKiroAuthMethodOption,
|
||||||
@@ -61,13 +67,16 @@ export function AddAccountDialog({
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [localError, setLocalError] = useState<string | null>(null);
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
const [acknowledgedRisk, setAcknowledgedRisk] = useState(false);
|
const [acknowledgedRisk, setAcknowledgedRisk] = useState(false);
|
||||||
|
const [agyRiskChecklist, setAgyRiskChecklist] = useState(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||||
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
const [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
||||||
const wasAuthenticatingRef = useRef(false);
|
const wasAuthenticatingRef = useRef(false);
|
||||||
const authFlow = useCliproxyAuthFlow();
|
const authFlow = useCliproxyAuthFlow();
|
||||||
const kiroImportMutation = useKiroImport();
|
const kiroImportMutation = useKiroImport();
|
||||||
|
|
||||||
const isKiro = provider === 'kiro';
|
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 defaultDeviceCode = isDeviceCodeProvider(provider);
|
||||||
const requiresNickname = isNicknameRequiredProvider(provider);
|
const requiresNickname = isNicknameRequiredProvider(provider);
|
||||||
const kiroMethodOption = getKiroAuthMethodOption(kiroAuthMethod);
|
const kiroMethodOption = getKiroAuthMethodOption(kiroAuthMethod);
|
||||||
@@ -82,6 +91,7 @@ export function AddAccountDialog({
|
|||||||
setCopied(false);
|
setCopied(false);
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
setAcknowledgedRisk(false);
|
setAcknowledgedRisk(false);
|
||||||
|
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||||
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
||||||
wasAuthenticatingRef.current = false;
|
wasAuthenticatingRef.current = false;
|
||||||
onClose();
|
onClose();
|
||||||
@@ -90,6 +100,7 @@ export function AddAccountDialog({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setAcknowledgedRisk(false);
|
setAcknowledgedRisk(false);
|
||||||
|
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
}
|
}
|
||||||
}, [provider, open]);
|
}, [provider, open]);
|
||||||
@@ -142,6 +153,12 @@ export function AddAccountDialog({
|
|||||||
* - Authorization code providers use /start-url and polling.
|
* - Authorization code providers use /start-url and polling.
|
||||||
*/
|
*/
|
||||||
const handleAuthenticate = () => {
|
const handleAuthenticate = () => {
|
||||||
|
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
||||||
|
setLocalError(
|
||||||
|
'Complete all Antigravity responsibility steps before authenticating this provider.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (requiresSafetyAcknowledgement && !acknowledgedRisk) {
|
if (requiresSafetyAcknowledgement && !acknowledgedRisk) {
|
||||||
setLocalError(
|
setLocalError(
|
||||||
'Please acknowledge the account safety warning before authenticating this provider.'
|
'Please acknowledge the account safety warning before authenticating this provider.'
|
||||||
@@ -159,6 +176,15 @@ export function AddAccountDialog({
|
|||||||
kiroMethod: isKiro ? kiroAuthMethod : undefined,
|
kiroMethod: isKiro ? kiroAuthMethod : undefined,
|
||||||
flowType: isKiro ? kiroMethodOption.flowType : undefined,
|
flowType: isKiro ? kiroMethodOption.flowType : undefined,
|
||||||
startEndpoint: isKiro ? kiroMethodOption.startEndpoint : 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>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
|
{requiresAgyResponsibilityFlow && !showAuthUI && (
|
||||||
|
<AntigravityResponsibilityChecklist
|
||||||
|
value={agyRiskChecklist}
|
||||||
|
onChange={(value) => {
|
||||||
|
setAgyRiskChecklist(value);
|
||||||
|
setLocalError(null);
|
||||||
|
}}
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{requiresSafetyAcknowledgement && !showAuthUI && (
|
{requiresSafetyAcknowledgement && !showAuthUI && (
|
||||||
<AccountSafetyWarningCard
|
<AccountSafetyWarningCard
|
||||||
|
provider="gemini"
|
||||||
showAcknowledgement
|
showAcknowledgement
|
||||||
acknowledged={acknowledgedRisk}
|
acknowledged={acknowledgedRisk}
|
||||||
onAcknowledgedChange={(value) => {
|
onAcknowledgedChange={(value) => {
|
||||||
@@ -406,6 +444,7 @@ export function AddAccountDialog({
|
|||||||
disabled={
|
disabled={
|
||||||
isPending ||
|
isPending ||
|
||||||
(requiresNickname && !nicknameTrimmed) ||
|
(requiresNickname && !nicknameTrimmed) ||
|
||||||
|
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
|
||||||
(requiresSafetyAcknowledgement && !acknowledgedRisk)
|
(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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,6 +28,13 @@ interface StartAuthOptions {
|
|||||||
kiroMethod?: string;
|
kiroMethod?: string;
|
||||||
flowType?: 'authorization_code' | 'device_code';
|
flowType?: 'authorization_code' | 'device_code';
|
||||||
startEndpoint?: 'start' | 'start-url';
|
startEndpoint?: 'start' | 'start-url';
|
||||||
|
riskAcknowledgement?: {
|
||||||
|
version: string;
|
||||||
|
reviewedIssue622: boolean;
|
||||||
|
understandsBanRisk: boolean;
|
||||||
|
acceptsFullResponsibility: boolean;
|
||||||
|
typedPhrase: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Polling interval for OAuth status check (3 seconds) */
|
/** Polling interval for OAuth status check (3 seconds) */
|
||||||
@@ -175,6 +182,7 @@ export function useCliproxyAuthFlow() {
|
|||||||
const payload = {
|
const payload = {
|
||||||
nickname: options?.nickname,
|
nickname: options?.nickname,
|
||||||
kiroMethod: options?.kiroMethod,
|
kiroMethod: options?.kiroMethod,
|
||||||
|
riskAcknowledgement: options?.riskAcknowledgement,
|
||||||
};
|
};
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export function CliproxyPage() {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim();
|
.trim();
|
||||||
const showAccountSafetyWarning = warningProvider === 'gemini' || warningProvider === 'agy';
|
const showAccountSafetyWarning = warningProvider === 'gemini' || warningProvider === 'agy';
|
||||||
|
const warningProviderType = warningProvider === 'agy' ? 'agy' : 'gemini';
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
|
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
|
||||||
@@ -394,7 +395,9 @@ 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 && <AccountSafetyWarningCard className="mx-4 mt-4" />}
|
{showAccountSafetyWarning && (
|
||||||
|
<AccountSafetyWarningCard provider={warningProviderType} className="mx-4 mt-4" />
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedVariantData && parentAuthForVariant ? (
|
{selectedVariantData && parentAuthForVariant ? (
|
||||||
// Variant selected - show ProviderEditor with variant profile name
|
// Variant selected - show ProviderEditor with variant profile name
|
||||||
|
|||||||
Reference in new issue
Block a user