mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 20:15:32 +00:00
Merge pull request #623 from kaitranntt/kai/fix/agy-responsibility-gate
fix(agy): enforce multi-step responsibility acknowledgement for antigravity oauth
This commit is contained in:
19 files changed
+1465
-167
No files matched your search
@@ -1,24 +1,45 @@
|
||||
import { AlertTriangle, ExternalLink } from 'lucide-react';
|
||||
import { AlertTriangle, ExternalLink, Settings2 } 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 { cn } from '@/lib/utils';
|
||||
import { RISK_ACK_PHRASE } from '@/components/account/antigravity-responsibility-constants';
|
||||
|
||||
interface AccountSafetyWarningCardProps {
|
||||
className?: string;
|
||||
showAcknowledgement?: boolean;
|
||||
acknowledged?: boolean;
|
||||
onAcknowledgedChange?: (value: boolean) => void;
|
||||
acknowledgementPhrase?: string;
|
||||
acknowledgementText?: string;
|
||||
onAcknowledgementTextChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
showProxySettingsLink?: boolean;
|
||||
}
|
||||
|
||||
export function AccountSafetyWarningCard({
|
||||
className,
|
||||
showAcknowledgement = false,
|
||||
acknowledged = false,
|
||||
onAcknowledgedChange,
|
||||
acknowledgementPhrase = RISK_ACK_PHRASE,
|
||||
acknowledgementText = '',
|
||||
onAcknowledgementTextChange,
|
||||
disabled = false,
|
||||
showProxySettingsLink = false,
|
||||
}: AccountSafetyWarningCardProps) {
|
||||
const title = 'OAuth Account Safety Warning';
|
||||
const subtitle = 'Issue #509 · Gemini + AGY OAuth risk';
|
||||
const firstLine = (
|
||||
<>
|
||||
Issue #509 documents suspension/ban reports tied to <code className="font-mono">ccs agy</code>{' '}
|
||||
and shared-account usage between <code className="font-mono">ccs gemini</code> and{' '}
|
||||
<code className="font-mono">ccs agy</code>.
|
||||
</>
|
||||
);
|
||||
const secondLine = (
|
||||
<>Continue only if you accept full responsibility for OAuth and account-access risk.</>
|
||||
);
|
||||
const issueUrl = 'https://github.com/kaitranntt/ccs/issues/509';
|
||||
const issueLabel = 'Read issue #509';
|
||||
const proxySettingsLabel = 'Gemini + AGY controls: Settings > Proxy';
|
||||
|
||||
return (
|
||||
<section
|
||||
role="alert"
|
||||
@@ -36,10 +57,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 +70,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,33 +80,44 @@ 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>
|
||||
{showProxySettingsLink && (
|
||||
<a
|
||||
href="/settings?tab=proxy"
|
||||
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"
|
||||
>
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
{proxySettingsLabel}
|
||||
</a>
|
||||
)}
|
||||
<span className="rounded-md border border-border/70 bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground">
|
||||
Applies to CLI and dashboard auth
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showAcknowledgement && onAcknowledgedChange && (
|
||||
{showAcknowledgement && onAcknowledgementTextChange && (
|
||||
<div className="rounded-lg border border-amber-500/25 bg-amber-500/5 p-2.5">
|
||||
<div className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id="account-risk-ack"
|
||||
checked={acknowledged}
|
||||
onCheckedChange={(checked) => onAcknowledgedChange(Boolean(checked))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label htmlFor="account-risk-ack" className="text-xs leading-5">
|
||||
I understand this risk and that CCS takes no responsibility if I continue this
|
||||
setup.
|
||||
</Label>
|
||||
</div>
|
||||
<Label htmlFor="account-risk-ack-text" className="text-xs leading-5">
|
||||
Type exact phrase to continue:{' '}
|
||||
<code className="rounded bg-muted px-1 py-0.5 font-mono">
|
||||
{acknowledgementPhrase}
|
||||
</code>
|
||||
</Label>
|
||||
<Input
|
||||
id="account-risk-ack-text"
|
||||
value={acknowledgementText}
|
||||
onChange={(e) => onAcknowledgementTextChange(e.target.value)}
|
||||
placeholder={acknowledgementPhrase}
|
||||
disabled={disabled}
|
||||
className="mt-2 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* For Kiro: Also shows "Import from IDE" option.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -25,11 +25,18 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Loader2, ExternalLink, User, Download, Copy, Check } from 'lucide-react';
|
||||
import { Loader2, ExternalLink, User, Download, Copy, Check, ShieldAlert } from 'lucide-react';
|
||||
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,
|
||||
RISK_ACK_PHRASE,
|
||||
isAntigravityRiskChecklistComplete,
|
||||
} from '@/components/account/antigravity-responsibility-constants';
|
||||
import {
|
||||
DEFAULT_KIRO_AUTH_METHOD,
|
||||
getKiroAuthMethodOption,
|
||||
@@ -49,6 +56,10 @@ interface AddAccountDialogProps {
|
||||
isFirstAccount?: boolean;
|
||||
}
|
||||
|
||||
function normalizeRiskPhrase(value: string): string {
|
||||
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
||||
}
|
||||
|
||||
export function AddAccountDialog({
|
||||
open,
|
||||
onClose,
|
||||
@@ -60,14 +71,21 @@ export function AddAccountDialog({
|
||||
const [callbackUrl, setCallbackUrl] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const [acknowledgedRisk, setAcknowledgedRisk] = useState(false);
|
||||
const [riskAcknowledgementText, setRiskAcknowledgementText] = useState('');
|
||||
const [agyRiskChecklist, setAgyRiskChecklist] = useState(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||
const [agyAckBypassEnabled, setAgyAckBypassEnabled] = useState(false);
|
||||
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(false);
|
||||
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' && !agyAckBypassEnabled;
|
||||
const isAgyBypassStatePending = provider === 'agy' && agyAckBypassLoading;
|
||||
const isAgyRiskChecklistComplete = isAntigravityRiskChecklistComplete(agyRiskChecklist);
|
||||
const isGeminiRiskAcknowledged = normalizeRiskPhrase(riskAcknowledgementText) === RISK_ACK_PHRASE;
|
||||
const defaultDeviceCode = isDeviceCodeProvider(provider);
|
||||
const requiresNickname = isNicknameRequiredProvider(provider);
|
||||
const kiroMethodOption = getKiroAuthMethodOption(kiroAuthMethod);
|
||||
@@ -76,12 +94,24 @@ export function AddAccountDialog({
|
||||
const nicknameTrimmed = nickname.trim();
|
||||
const errorMessage = localError || authFlow.error;
|
||||
|
||||
const fetchAgyBypassState = useCallback(async (): Promise<boolean> => {
|
||||
const response = await fetch('/api/settings/auth/antigravity-risk');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load Antigravity power user setting');
|
||||
}
|
||||
const data = (await response.json()) as { antigravityAckBypass?: boolean };
|
||||
return data.antigravityAckBypass === true;
|
||||
}, []);
|
||||
|
||||
const resetAndClose = () => {
|
||||
setNickname('');
|
||||
setCallbackUrl('');
|
||||
setCopied(false);
|
||||
setLocalError(null);
|
||||
setAcknowledgedRisk(false);
|
||||
setRiskAcknowledgementText('');
|
||||
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||
setAgyAckBypassEnabled(false);
|
||||
setAgyAckBypassLoading(false);
|
||||
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
||||
wasAuthenticatingRef.current = false;
|
||||
onClose();
|
||||
@@ -89,11 +119,87 @@ export function AddAccountDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setAcknowledgedRisk(false);
|
||||
setRiskAcknowledgementText('');
|
||||
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||
setLocalError(null);
|
||||
}
|
||||
}, [provider, open]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
if (!open || provider !== 'agy') {
|
||||
setAgyAckBypassEnabled(false);
|
||||
setAgyAckBypassLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadAgyBypassState = async () => {
|
||||
try {
|
||||
setAgyAckBypassLoading(true);
|
||||
const enabled = await fetchAgyBypassState();
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassEnabled(enabled);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassEnabled(false);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAgyBypassState();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fetchAgyBypassState, open, provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || provider !== 'agy' || !authFlow.error || !agyAckBypassEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedError = authFlow.error.toLowerCase();
|
||||
const ackRequired =
|
||||
normalizedError.includes('agy_risk_ack_required') ||
|
||||
normalizedError.includes('responsibility acknowledgement') ||
|
||||
normalizedError.includes('responsibility checklist');
|
||||
if (!ackRequired) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const syncBypassState = async () => {
|
||||
try {
|
||||
setAgyAckBypassLoading(true);
|
||||
const enabled = await fetchAgyBypassState();
|
||||
if (cancelled) return;
|
||||
setAgyAckBypassEnabled(enabled);
|
||||
if (!enabled) {
|
||||
setLocalError('Power user mode is off. Complete the AGY checklist and retry.');
|
||||
}
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setAgyAckBypassEnabled(false);
|
||||
setLocalError('Power user mode is off. Complete the AGY checklist and retry.');
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setAgyAckBypassLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void syncBypassState();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [agyAckBypassEnabled, authFlow.error, fetchAgyBypassState, open, provider]);
|
||||
|
||||
// When authFlow completes successfully (polling detected success), apply preset and close
|
||||
useEffect(() => {
|
||||
if (!authFlow.isAuthenticating && !authFlow.error && authFlow.provider === null && open) {
|
||||
@@ -142,9 +248,19 @@ export function AddAccountDialog({
|
||||
* - Authorization code providers use /start-url and polling.
|
||||
*/
|
||||
const handleAuthenticate = () => {
|
||||
if (requiresSafetyAcknowledgement && !acknowledgedRisk) {
|
||||
if (isAgyBypassStatePending) {
|
||||
setLocalError('Loading Antigravity safety settings. Please wait a moment and retry.');
|
||||
return;
|
||||
}
|
||||
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
||||
setLocalError(
|
||||
'Please acknowledge the account safety warning before authenticating this provider.'
|
||||
'Complete all Antigravity responsibility steps before authenticating this provider.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (requiresSafetyAcknowledgement && !isGeminiRiskAcknowledged) {
|
||||
setLocalError(
|
||||
`Type "${RISK_ACK_PHRASE}" to acknowledge the account safety warning before authenticating this provider.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -159,6 +275,15 @@ export function AddAccountDialog({
|
||||
kiroMethod: isKiro ? kiroAuthMethod : undefined,
|
||||
flowType: isKiro ? kiroMethodOption.flowType : undefined,
|
||||
startEndpoint: isKiro ? kiroMethodOption.startEndpoint : undefined,
|
||||
riskAcknowledgement: requiresAgyResponsibilityFlow
|
||||
? {
|
||||
version: ANTIGRAVITY_ACK_VERSION,
|
||||
reviewedIssue509: agyRiskChecklist.reviewedIssue509,
|
||||
understandsBanRisk: agyRiskChecklist.understandsBanRisk,
|
||||
acceptsFullResponsibility: agyRiskChecklist.acceptsFullResponsibility,
|
||||
typedPhrase: agyRiskChecklist.typedPhrase,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -204,12 +329,35 @@ export function AddAccountDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{requiresAgyResponsibilityFlow && !showAuthUI && (
|
||||
<AntigravityResponsibilityChecklist
|
||||
value={agyRiskChecklist}
|
||||
onChange={(value) => {
|
||||
setAgyRiskChecklist(value);
|
||||
setLocalError(null);
|
||||
}}
|
||||
disabled={isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{provider === 'agy' && agyAckBypassEnabled && !showAuthUI && (
|
||||
<div className="rounded-lg border border-amber-400/35 bg-amber-50/70 p-3 text-xs text-amber-900 dark:border-amber-800/60 dark:bg-amber-950/25 dark:text-amber-100">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 font-semibold">
|
||||
<ShieldAlert className="h-3.5 w-3.5" />
|
||||
Power user mode enabled
|
||||
</div>
|
||||
AGY responsibility checklist is skipped from Settings {'>'} Proxy. You accept full
|
||||
responsibility for OAuth/account risk.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requiresSafetyAcknowledgement && !showAuthUI && (
|
||||
<AccountSafetyWarningCard
|
||||
showAcknowledgement
|
||||
acknowledged={acknowledgedRisk}
|
||||
onAcknowledgedChange={(value) => {
|
||||
setAcknowledgedRisk(value);
|
||||
acknowledgementPhrase={RISK_ACK_PHRASE}
|
||||
acknowledgementText={riskAcknowledgementText}
|
||||
onAcknowledgementTextChange={(value) => {
|
||||
setRiskAcknowledgementText(value);
|
||||
setLocalError(null);
|
||||
}}
|
||||
disabled={isPending}
|
||||
@@ -405,8 +553,10 @@ export function AddAccountDialog({
|
||||
onClick={handleAuthenticate}
|
||||
disabled={
|
||||
isPending ||
|
||||
isAgyBypassStatePending ||
|
||||
(requiresNickname && !nicknameTrimmed) ||
|
||||
(requiresSafetyAcknowledgement && !acknowledgedRisk)
|
||||
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
|
||||
(requiresSafetyAcknowledgement && !isGeminiRiskAcknowledged)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
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 } from '@/components/account/antigravity-responsibility-constants';
|
||||
import type { 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.reviewedIssue509,
|
||||
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.reviewedIssue509}
|
||||
onCheckedChange={(checked) => setValue({ reviewedIssue509: Boolean(checked) })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Label htmlFor="agy-step-reviewed-issue" className="text-xs leading-5">
|
||||
Step 1: I reviewed issue #509 and understand AGY 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/509"
|
||||
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 #509
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export const ANTIGRAVITY_ACK_VERSION = '2026-02-24-antigravity-oauth-v2';
|
||||
export const RISK_ACK_PHRASE = 'I ACCEPT RISK';
|
||||
export const ANTIGRAVITY_ACK_PHRASE = RISK_ACK_PHRASE;
|
||||
|
||||
export interface AntigravityRiskChecklistValue {
|
||||
reviewedIssue509: boolean;
|
||||
understandsBanRisk: boolean;
|
||||
acceptsFullResponsibility: boolean;
|
||||
typedPhrase: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_ANTIGRAVITY_RISK_CHECKLIST: AntigravityRiskChecklistValue = {
|
||||
reviewedIssue509: false,
|
||||
understandsBanRisk: false,
|
||||
acceptsFullResponsibility: false,
|
||||
typedPhrase: '',
|
||||
};
|
||||
|
||||
export function isAntigravityRiskChecklistComplete(value: AntigravityRiskChecklistValue): boolean {
|
||||
return (
|
||||
value.reviewedIssue509 &&
|
||||
value.understandsBanRisk &&
|
||||
value.acceptsFullResponsibility &&
|
||||
value.typedPhrase.trim().replace(/\s+/g, ' ').toUpperCase() === ANTIGRAVITY_ACK_PHRASE
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,13 @@ interface StartAuthOptions {
|
||||
kiroMethod?: string;
|
||||
flowType?: 'authorization_code' | 'device_code';
|
||||
startEndpoint?: 'start' | 'start-url';
|
||||
riskAcknowledgement?: {
|
||||
version: string;
|
||||
reviewedIssue509: boolean;
|
||||
understandsBanRisk: boolean;
|
||||
acceptsFullResponsibility: boolean;
|
||||
typedPhrase: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** Polling interval for OAuth status check (3 seconds) */
|
||||
@@ -35,6 +42,19 @@ const POLL_INTERVAL = 3000;
|
||||
/** Maximum polling duration (5 minutes) */
|
||||
const MAX_POLL_DURATION = 5 * 60 * 1000;
|
||||
|
||||
async function parseResponseBody(response: Response): Promise<Record<string, unknown>> {
|
||||
const text = await response.text();
|
||||
if (!text) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(text) as Record<string, unknown>;
|
||||
} catch {
|
||||
const fallbackError =
|
||||
response.status >= 400 ? `Request failed with status ${response.status}` : undefined;
|
||||
return fallbackError ? { error: fallbackError } : {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Initial state for auth flow - extracted for DRY */
|
||||
const INITIAL_STATE: AuthFlowState = {
|
||||
provider: null,
|
||||
@@ -175,6 +195,7 @@ export function useCliproxyAuthFlow() {
|
||||
const payload = {
|
||||
nickname: options?.nickname,
|
||||
kiroMethod: options?.kiroMethod,
|
||||
riskAcknowledgement: options?.riskAcknowledgement,
|
||||
};
|
||||
|
||||
setState({
|
||||
@@ -198,8 +219,9 @@ export function useCliproxyAuthFlow() {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(async (response) => {
|
||||
const data = await response.json();
|
||||
if (response.ok && data.success) {
|
||||
const data = await parseResponseBody(response);
|
||||
const success = data.success === true;
|
||||
if (response.ok && success) {
|
||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
||||
// Note: No toast here - DeviceCodeDialog's useDeviceCode hook handles success toast
|
||||
@@ -207,7 +229,8 @@ export function useCliproxyAuthFlow() {
|
||||
openedAuthUrlRef.current = false;
|
||||
setState(INITIAL_STATE);
|
||||
} else {
|
||||
const errorMsg = data.error || 'Authentication failed';
|
||||
const errorMsg =
|
||||
typeof data.error === 'string' ? data.error : 'Authentication failed';
|
||||
toast.error(errorMsg);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -239,30 +262,35 @@ export function useCliproxyAuthFlow() {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await parseResponseBody(response);
|
||||
const success = data.success === true;
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || 'Failed to start OAuth');
|
||||
if (!response.ok || !success) {
|
||||
const errorMsg = typeof data.error === 'string' ? data.error : 'Failed to start OAuth';
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const authUrl = typeof data.authUrl === 'string' ? data.authUrl : null;
|
||||
const oauthState = typeof data.state === 'string' ? data.state : null;
|
||||
|
||||
// Update state with auth URL
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
authUrl: data.authUrl || null,
|
||||
oauthState: data.state,
|
||||
authUrl,
|
||||
oauthState,
|
||||
}));
|
||||
|
||||
// Auto-open auth URL in new browser tab (fallback URL still shown in dialog)
|
||||
if (data.authUrl) {
|
||||
if (authUrl) {
|
||||
openedAuthUrlRef.current = true;
|
||||
window.open(data.authUrl, '_blank');
|
||||
window.open(authUrl, '_blank');
|
||||
}
|
||||
|
||||
// Start polling for completion
|
||||
if (data.state) {
|
||||
if (oauthState) {
|
||||
pollStartRef.current = Date.now();
|
||||
pollIntervalRef.current = setInterval(() => {
|
||||
pollStatus(provider, data.state);
|
||||
pollStatus(provider, oauthState);
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
@@ -311,16 +339,19 @@ export function useCliproxyAuthFlow() {
|
||||
body: JSON.stringify({ redirectUrl }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const data = await parseResponseBody(response);
|
||||
const success = data.success === true;
|
||||
|
||||
if (response.ok && data.success) {
|
||||
if (response.ok && success) {
|
||||
stopPolling();
|
||||
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
|
||||
toast.success(`${state.provider} authentication successful`);
|
||||
setState(INITIAL_STATE);
|
||||
} else {
|
||||
throw new Error(data.error || 'Callback submission failed');
|
||||
const errorMsg =
|
||||
typeof data.error === 'string' ? data.error : 'Callback submission failed';
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to submit callback';
|
||||
|
||||
@@ -397,7 +397,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 showProxySettingsLink className="mx-4 mt-4" />
|
||||
)}
|
||||
|
||||
{selectedVariantData && parentAuthForVariant ? (
|
||||
// Variant selected - show ProviderEditor with variant profile name
|
||||
|
||||
@@ -197,6 +197,13 @@ export default function AuthSection() {
|
||||
setTimeout(() => setCopiedSecret(false), 2000);
|
||||
};
|
||||
|
||||
const refreshAll = async () => {
|
||||
if (loading || saving) return;
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
await Promise.all([fetchTokens(), fetchRawConfig()]);
|
||||
};
|
||||
|
||||
if (loading || !tokens) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
@@ -359,7 +366,6 @@ export default function AuthSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -383,10 +389,7 @@ export default function AuthSection() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
fetchTokens();
|
||||
fetchRawConfig();
|
||||
}}
|
||||
onClick={refreshAll}
|
||||
disabled={loading || saving}
|
||||
className="flex-1"
|
||||
>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Settings section for CLIProxyAPI configuration (local/remote)
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import {
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
Bug,
|
||||
Box,
|
||||
AlertTriangle,
|
||||
ShieldAlert,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { useProxyConfig, useRawConfig } from '../../hooks';
|
||||
import { useUpdateBackend, useProxyStatus } from '@/hooks/use-cliproxy';
|
||||
@@ -25,6 +28,7 @@ import { RemoteProxyCard } from './remote-proxy-card';
|
||||
import { ProxyStatusWidget } from '@/components/monitoring/proxy-status-widget';
|
||||
import { api } from '@/lib/api-client';
|
||||
import { CLIPROXY_DEFAULT_PORT } from '@/lib/preset-utils';
|
||||
import { RISK_ACK_PHRASE } from '@/components/account/antigravity-responsibility-constants';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/** LocalStorage key for debug mode preference */
|
||||
@@ -33,6 +37,10 @@ const DEBUG_MODE_KEY = 'ccs_debug_mode';
|
||||
/** Providers only available on CLIProxyAPIPlus */
|
||||
const PLUS_ONLY_PROVIDERS = ['kiro', 'ghcp'];
|
||||
|
||||
function normalizeRiskAckPhrase(value: string): string {
|
||||
return value.trim().replace(/\s+/g, ' ').toUpperCase();
|
||||
}
|
||||
|
||||
export default function ProxySection() {
|
||||
const {
|
||||
config,
|
||||
@@ -67,6 +75,14 @@ export default function ProxySection() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [agyAckBypass, setAgyAckBypass] = useState(false);
|
||||
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(true);
|
||||
const [agyAckBypassSaving, setAgyAckBypassSaving] = useState(false);
|
||||
const [showAgyEnableConfirm, setShowAgyEnableConfirm] = useState(false);
|
||||
const [agyEnableConfirmPhrase, setAgyEnableConfirmPhrase] = useState('');
|
||||
const agyAckBypassSavingRef = useRef(false);
|
||||
const isAgyConfirmPhraseValid =
|
||||
normalizeRiskAckPhrase(agyEnableConfirmPhrase) === RISK_ACK_PHRASE;
|
||||
|
||||
const handleDebugModeChange = (enabled: boolean) => {
|
||||
setDebugMode(enabled);
|
||||
@@ -77,6 +93,101 @@ export default function ProxySection() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAgyAckBypass = useCallback(async () => {
|
||||
try {
|
||||
setAgyAckBypassLoading(true);
|
||||
const response = await fetch('/api/settings/auth/antigravity-risk');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load AGY power user mode');
|
||||
}
|
||||
const data = (await response.json()) as { antigravityAckBypass?: boolean };
|
||||
setAgyAckBypass(data.antigravityAckBypass === true);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to load AGY power user mode');
|
||||
setAgyAckBypass(false);
|
||||
} finally {
|
||||
setAgyAckBypassLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const persistAgyAckBypass = useCallback(
|
||||
async (nextValue: boolean) => {
|
||||
if (agyAckBypassSavingRef.current || agyAckBypassSaving || saving) return;
|
||||
|
||||
try {
|
||||
agyAckBypassSavingRef.current = true;
|
||||
setAgyAckBypassSaving(true);
|
||||
|
||||
const response = await fetch('/api/settings/auth/antigravity-risk', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ antigravityAckBypass: nextValue }),
|
||||
});
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
antigravityAckBypass?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || 'Failed to update AGY power user mode');
|
||||
}
|
||||
|
||||
const persistedValue = payload.antigravityAckBypass === true;
|
||||
|
||||
const verifyResponse = await fetch('/api/settings/auth/antigravity-risk', {
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (!verifyResponse.ok) {
|
||||
throw new Error('Failed to verify AGY power user mode persistence');
|
||||
}
|
||||
const verifyData = (await verifyResponse.json()) as { antigravityAckBypass?: boolean };
|
||||
const verifiedValue = verifyData.antigravityAckBypass === true;
|
||||
if (verifiedValue !== nextValue) {
|
||||
throw new Error(
|
||||
'AGY power user mode was not persisted. Config may have been modified by another process.'
|
||||
);
|
||||
}
|
||||
|
||||
setAgyAckBypass(verifiedValue && persistedValue);
|
||||
setShowAgyEnableConfirm(false);
|
||||
setAgyEnableConfirmPhrase('');
|
||||
toast.success(nextValue ? 'AGY power user mode enabled.' : 'AGY power user mode disabled.');
|
||||
await fetchRawConfig();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to update AGY power user mode');
|
||||
} finally {
|
||||
agyAckBypassSavingRef.current = false;
|
||||
setAgyAckBypassSaving(false);
|
||||
}
|
||||
},
|
||||
[agyAckBypassSaving, fetchRawConfig, saving]
|
||||
);
|
||||
|
||||
const handleAgyAckBypassChange = useCallback(
|
||||
(nextValue: boolean) => {
|
||||
if (agyAckBypassSavingRef.current || agyAckBypassSaving || saving) return;
|
||||
|
||||
if (nextValue) {
|
||||
setShowAgyEnableConfirm(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowAgyEnableConfirm(false);
|
||||
setAgyEnableConfirmPhrase('');
|
||||
void persistAgyAckBypass(false);
|
||||
},
|
||||
[agyAckBypassSaving, persistAgyAckBypass, saving]
|
||||
);
|
||||
|
||||
const confirmAgyEnable = useCallback(() => {
|
||||
if (!isAgyConfirmPhraseValid) {
|
||||
toast.error(`Type "${RISK_ACK_PHRASE}" to continue.`);
|
||||
return;
|
||||
}
|
||||
void persistAgyAckBypass(true);
|
||||
}, [isAgyConfirmPhraseValid, persistAgyAckBypass]);
|
||||
|
||||
// Backend state (loaded from API) + mutation hook for proper query invalidation
|
||||
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
|
||||
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
|
||||
@@ -138,12 +249,13 @@ export default function ProxySection() {
|
||||
// Load data on mount
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
void fetchAgyAckBypass();
|
||||
fetchRawConfig();
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended
|
||||
|
||||
void fetchBackend();
|
||||
|
||||
void checkPlusOnlyVariants();
|
||||
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
|
||||
}, [fetchConfig, fetchAgyAckBypass, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
|
||||
|
||||
if (loading || !config) {
|
||||
return (
|
||||
@@ -403,6 +515,116 @@ export default function ProxySection() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Safety */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-base font-medium flex items-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4 text-amber-700 dark:text-amber-300" />
|
||||
Safety
|
||||
</h3>
|
||||
<div className="space-y-3 rounded-lg border border-amber-400/35 bg-amber-50/70 p-4 dark:border-amber-800/60 dark:bg-amber-950/25">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-sm">Antigravity Power User Mode</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Skip AGY responsibility checklist in Add Account and `ccs agy` flows.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
aria-labelledby="agy-power-user-mode-label"
|
||||
aria-describedby="agy-power-user-mode-description"
|
||||
checked={agyAckBypass}
|
||||
disabled={agyAckBypassLoading || agyAckBypassSaving || saving}
|
||||
onCheckedChange={handleAgyAckBypassChange}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
id="agy-power-user-mode-description"
|
||||
className="text-xs text-amber-800/90 dark:text-amber-200/90"
|
||||
>
|
||||
Use only if you fully understand the OAuth suspension/ban risk pattern (#509). CCS
|
||||
cannot assume responsibility for account loss.
|
||||
</p>
|
||||
{showAgyEnableConfirm && (
|
||||
<div className="space-y-3 rounded-lg border border-rose-500/40 bg-rose-500/[0.08] p-3.5">
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-semibold tracking-wide text-rose-900 dark:text-rose-200">
|
||||
Final confirmation required
|
||||
</p>
|
||||
<p className="text-xs leading-relaxed text-rose-800/95 dark:text-rose-200/90">
|
||||
Enabling this will skip AGY safety checkpoints in both dashboard and CLI.
|
||||
Review issue #509 and type the exact phrase to proceed.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
<div className="rounded-md border border-rose-400/30 bg-rose-500/10 p-2.5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-rose-900 dark:text-rose-200">
|
||||
Step 1
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/kaitranntt/ccs/issues/509"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-1 inline-flex items-center gap-1.5 text-xs font-medium text-rose-800 underline decoration-rose-500/60 underline-offset-2 transition-colors hover:text-rose-700 dark:text-rose-200"
|
||||
>
|
||||
Read issue #509
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="rounded-md border border-rose-400/30 bg-rose-500/10 p-2.5">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wide text-rose-900 dark:text-rose-200">
|
||||
Step 2
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-rose-800/95 dark:text-rose-200/90">
|
||||
Type{' '}
|
||||
<code className="rounded bg-background/80 px-1 py-0.5 font-mono">
|
||||
{RISK_ACK_PHRASE}
|
||||
</code>{' '}
|
||||
to enable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={agyEnableConfirmPhrase}
|
||||
onChange={(e) => setAgyEnableConfirmPhrase(e.target.value)}
|
||||
placeholder={RISK_ACK_PHRASE}
|
||||
disabled={agyAckBypassSaving || saving}
|
||||
className="font-mono text-xs"
|
||||
aria-label="Type I ACCEPT RISK to enable Antigravity power user mode"
|
||||
/>
|
||||
<p className="text-[11px] text-rose-800/90 dark:text-rose-200/80">
|
||||
Exact phrase required.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowAgyEnableConfirm(false);
|
||||
setAgyEnableConfirmPhrase('');
|
||||
}}
|
||||
disabled={agyAckBypassSaving || saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={confirmAgyEnable}
|
||||
disabled={!isAgyConfirmPhraseValid || agyAckBypassSaving || saving}
|
||||
>
|
||||
Enable Power User Mode
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span id="agy-power-user-mode-label" className="sr-only">
|
||||
Toggle AGY power user mode
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remote Settings - Show when remote mode is enabled */}
|
||||
{isRemoteMode && (
|
||||
<RemoteProxyCard
|
||||
@@ -517,11 +739,12 @@ export default function ProxySection() {
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
fetchConfig();
|
||||
fetchAgyAckBypass();
|
||||
fetchRawConfig();
|
||||
fetchBackend();
|
||||
checkPlusOnlyVariants();
|
||||
}}
|
||||
disabled={loading || saving}
|
||||
disabled={loading || saving || agyAckBypassSaving}
|
||||
className="w-full"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
|
||||
Reference in new issue
Block a user