feat(agy): add power-user bypass for responsibility acknowledgement

This commit is contained in:
Tam Nhu Tran
2026-02-24 17:42:57 +07:00
parent 7b4bd804c8
commit d3c271ab2c
8 changed files with 299 additions and 8 deletions
+15 -1
View File
@@ -10,6 +10,7 @@
import { createInterface, Interface } from 'readline';
import { fail, info, ok, warn } from '../utils/ui';
import { getCliproxySafetyConfig } from '../config/unified-config-loader';
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';
@@ -47,6 +48,19 @@ function isTruthyEnv(value: string | undefined): boolean {
return normalized === '1' || normalized === 'true' || normalized === 'yes';
}
export function isAntigravityResponsibilityBypassEnabled(): boolean {
if (isTruthyEnv(process.env.CCS_ACCEPT_AGY_RISK)) {
return true;
}
try {
const safety = getCliproxySafetyConfig();
return safety.antigravity_ack_bypass === true;
} catch {
return false;
}
}
function askQuestion(rl: Interface, prompt: string): Promise<string | null> {
return new Promise((resolve) => {
let settled = false;
@@ -160,7 +174,7 @@ export function validateAntigravityRiskAcknowledgement(payload: unknown): Valida
export async function ensureCliAntigravityResponsibility(
options: EnsureCliRiskOptions
): Promise<boolean> {
if (options.acceptedByFlag || isTruthyEnv(process.env.CCS_ACCEPT_AGY_RISK)) {
if (options.acceptedByFlag || isAntigravityResponsibilityBypassEnabled()) {
return true;
}
+20
View File
@@ -18,10 +18,12 @@ import {
DEFAULT_CURSOR_CONFIG,
DEFAULT_GLOBAL_ENV,
DEFAULT_CLIPROXY_SERVER_CONFIG,
DEFAULT_CLIPROXY_SAFETY_CONFIG,
DEFAULT_QUOTA_MANAGEMENT_CONFIG,
DEFAULT_THINKING_CONFIG,
DEFAULT_DASHBOARD_AUTH_CONFIG,
DEFAULT_IMAGE_ANALYSIS_CONFIG,
CLIProxySafetyConfig,
GlobalEnvConfig,
ThinkingConfig,
DashboardAuthConfig,
@@ -249,6 +251,11 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
request_log:
partial.cliproxy?.logging?.request_log ?? defaults.cliproxy.logging?.request_log ?? false,
},
safety: {
antigravity_ack_bypass:
partial.cliproxy?.safety?.antigravity_ack_bypass ??
DEFAULT_CLIPROXY_SAFETY_CONFIG.antigravity_ack_bypass,
},
// Auth config - preserve user values, no defaults (uses constants as fallback)
auth: partial.cliproxy?.auth,
// Backend selection - validate and preserve user choice (original vs plus)
@@ -892,6 +899,19 @@ export function getGlobalEnvConfig(): GlobalEnvConfig {
};
}
/**
* Get cliproxy safety configuration.
* Returns defaults if not configured.
*/
export function getCliproxySafetyConfig(): CLIProxySafetyConfig {
const config = loadOrCreateUnifiedConfig();
return {
antigravity_ack_bypass:
config.cliproxy?.safety?.antigravity_ack_bypass ??
DEFAULT_CLIPROXY_SAFETY_CONFIG.antigravity_ack_bypass,
};
}
/**
* Get thinking configuration.
* Returns defaults if not configured.
+19
View File
@@ -156,6 +156,22 @@ export interface CLIProxyLoggingConfig {
request_log?: boolean;
}
/**
* CLIProxy safety configuration.
* Controls high-risk flow safeguards for supported providers.
*/
export interface CLIProxySafetyConfig {
/** Allow skipping AGY responsibility acknowledgement flow (default: false) */
antigravity_ack_bypass?: boolean;
}
/**
* Default CLIProxy safety configuration.
*/
export const DEFAULT_CLIPROXY_SAFETY_CONFIG: CLIProxySafetyConfig = {
antigravity_ack_bypass: false,
};
/**
* Token refresh configuration.
* Manages background token refresh worker settings.
@@ -187,6 +203,8 @@ export interface CLIProxyConfig {
variants: Record<string, CLIProxyVariantConfig | CompositeVariantConfig>;
/** Logging configuration (disabled by default) */
logging?: CLIProxyLoggingConfig;
/** Safety controls for high-risk provider flows */
safety?: CLIProxySafetyConfig;
/** Kiro: disable incognito browser mode (use normal browser to save credentials) */
kiro_no_incognito?: boolean;
/** Global auth configuration for CLIProxyAPI */
@@ -777,6 +795,7 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
enabled: false,
request_log: false,
},
safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG },
auto_sync: true,
},
preferences: {
@@ -46,7 +46,10 @@ import {
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
import { validateAntigravityRiskAcknowledgement } from '../../cliproxy/antigravity-responsibility';
import {
validateAntigravityRiskAcknowledgement,
isAntigravityResponsibilityBypassEnabled,
} from '../../cliproxy/antigravity-responsibility';
const router = Router();
@@ -406,7 +409,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
return;
}
if (provider === 'agy') {
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
if (!validation.valid) {
res.status(400).json({
@@ -634,7 +637,7 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
return;
}
if (provider === 'agy') {
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
if (!validation.valid) {
res.status(400).json({
+43
View File
@@ -19,6 +19,7 @@ import {
} from '../../cliproxy';
import { regenerateConfig } from '../../cliproxy/config-generator';
import { deduplicateCcsHooks } from '../../utils/websearch/hook-utils';
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
import type { Settings } from '../../types/config';
const router = Router();
@@ -383,6 +384,48 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void =>
// ==================== Auth Tokens ====================
/**
* GET /api/settings/auth/antigravity-risk - Get AGY responsibility bypass setting
*/
router.get('/auth/antigravity-risk', (_req: Request, res: Response): void => {
try {
const config = loadOrCreateUnifiedConfig();
res.json({
antigravityAckBypass: config.cliproxy?.safety?.antigravity_ack_bypass === true,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* PUT /api/settings/auth/antigravity-risk - Update AGY responsibility bypass setting
*/
router.put('/auth/antigravity-risk', (req: Request, res: Response): void => {
try {
const { antigravityAckBypass } = req.body as { antigravityAckBypass?: unknown };
if (typeof antigravityAckBypass !== 'boolean') {
res.status(400).json({ error: 'antigravityAckBypass must be a boolean' });
return;
}
const config = loadOrCreateUnifiedConfig();
config.cliproxy.safety = {
...(config.cliproxy.safety ?? {}),
antigravity_ack_bypass: antigravityAckBypass,
};
saveUnifiedConfig(config);
res.json({
success: true,
antigravityAckBypass,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* GET /api/settings/auth/tokens - Get current auth token status (masked)
*/
@@ -1,12 +1,44 @@
import { describe, expect, it } from 'bun:test';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
ANTIGRAVITY_ACK_PHRASE,
ANTIGRAVITY_ACK_VERSION,
hasAntigravityRiskAcceptanceFlag,
isAntigravityResponsibilityBypassEnabled,
validateAntigravityRiskAcknowledgement,
} from '../../../src/cliproxy/antigravity-responsibility';
describe('antigravity-responsibility', () => {
let tempHome = '';
let originalCcsHome: string | undefined;
let originalAgyRiskEnv: string | undefined;
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-agy-risk-test-'));
originalCcsHome = process.env.CCS_HOME;
originalAgyRiskEnv = process.env.CCS_ACCEPT_AGY_RISK;
process.env.CCS_HOME = tempHome;
delete process.env.CCS_ACCEPT_AGY_RISK;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
if (originalAgyRiskEnv !== undefined) {
process.env.CCS_ACCEPT_AGY_RISK = originalAgyRiskEnv;
} else {
delete process.env.CCS_ACCEPT_AGY_RISK;
}
fs.rmSync(tempHome, { recursive: true, force: true });
});
it('accepts a complete acknowledgement payload', () => {
const result = validateAntigravityRiskAcknowledgement({
version: ANTIGRAVITY_ACK_VERSION,
@@ -75,4 +107,24 @@ describe('antigravity-responsibility', () => {
expect(hasAntigravityRiskAcceptanceFlag(['--accept-antigravity-risk'])).toBeTrue();
expect(hasAntigravityRiskAcceptanceFlag(['--auth'])).toBeFalse();
});
it('enables bypass when CCS_ACCEPT_AGY_RISK is set', () => {
process.env.CCS_ACCEPT_AGY_RISK = 'true';
expect(isAntigravityResponsibilityBypassEnabled()).toBeTrue();
});
it('enables bypass when cliproxy safety setting is enabled', () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
`version: 8
cliproxy:
safety:
antigravity_ack_bypass: true
`
);
expect(isAntigravityResponsibilityBypassEnabled()).toBeTrue();
});
});
@@ -25,7 +25,7 @@ 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';
@@ -68,6 +68,8 @@ export function AddAccountDialog({
const [localError, setLocalError] = useState<string | null>(null);
const [acknowledgedRisk, setAcknowledgedRisk] = useState(false);
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();
@@ -75,7 +77,8 @@ export function AddAccountDialog({
const isKiro = provider === 'kiro';
const requiresSafetyAcknowledgement = provider === 'gemini';
const requiresAgyResponsibilityFlow = provider === 'agy';
const requiresAgyResponsibilityFlow = provider === 'agy' && !agyAckBypassEnabled;
const isAgyBypassStatePending = provider === 'agy' && agyAckBypassLoading;
const isAgyRiskChecklistComplete = isAntigravityRiskChecklistComplete(agyRiskChecklist);
const defaultDeviceCode = isDeviceCodeProvider(provider);
const requiresNickname = isNicknameRequiredProvider(provider);
@@ -92,6 +95,8 @@ export function AddAccountDialog({
setLocalError(null);
setAcknowledgedRisk(false);
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
setAgyAckBypassEnabled(false);
setAgyAckBypassLoading(false);
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
wasAuthenticatingRef.current = false;
onClose();
@@ -105,6 +110,44 @@ export function AddAccountDialog({
}
}, [provider, open]);
useEffect(() => {
let cancelled = false;
if (!open || provider !== 'agy') {
setAgyAckBypassEnabled(false);
setAgyAckBypassLoading(false);
return;
}
const loadAgyBypassState = async () => {
try {
setAgyAckBypassLoading(true);
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 };
if (!cancelled) {
setAgyAckBypassEnabled(data.antigravityAckBypass === true);
}
} catch {
if (!cancelled) {
setAgyAckBypassEnabled(false);
}
} finally {
if (!cancelled) {
setAgyAckBypassLoading(false);
}
}
};
loadAgyBypassState();
return () => {
cancelled = true;
};
}, [open, provider]);
// When authFlow completes successfully (polling detected success), apply preset and close
useEffect(() => {
if (!authFlow.isAuthenticating && !authFlow.error && authFlow.provider === null && open) {
@@ -153,6 +196,10 @@ export function AddAccountDialog({
* - Authorization code providers use /start-url and polling.
*/
const handleAuthenticate = () => {
if (isAgyBypassStatePending) {
setLocalError('Loading Antigravity safety settings. Please wait a moment and retry.');
return;
}
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
setLocalError(
'Complete all Antigravity responsibility steps before authenticating this provider.'
@@ -241,6 +288,17 @@ export function AddAccountDialog({
/>
)}
{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 {'>'} Auth. You accept full
responsibility for OAuth/account risk.
</div>
)}
{requiresSafetyAcknowledgement && !showAuthUI && (
<AccountSafetyWarningCard
provider="gemini"
@@ -443,6 +501,7 @@ export function AddAccountDialog({
onClick={handleAuthenticate}
disabled={
isPending ||
isAgyBypassStatePending ||
(requiresNickname && !nicknameTrimmed) ||
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
(requiresSafetyAcknowledgement && !acknowledgedRisk)
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import {
RefreshCw,
CheckCircle2,
@@ -20,6 +21,7 @@ import {
Check,
KeyRound,
ShieldCheck,
ShieldAlert,
Save,
} from 'lucide-react';
import { useRawConfig } from '../hooks';
@@ -51,6 +53,9 @@ export default function AuthSection() {
const [editedSecret, setEditedSecret] = useState<string | null>(null);
const [copiedApiKey, setCopiedApiKey] = useState(false);
const [copiedSecret, setCopiedSecret] = useState(false);
const [agyAckBypass, setAgyAckBypass] = useState(false);
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(true);
const [agyAckBypassSaving, setAgyAckBypassSaving] = useState(false);
// Fetch tokens
const fetchTokens = useCallback(async () => {
@@ -71,11 +76,28 @@ export default function AuthSection() {
}
}, []);
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 Antigravity power user settings');
}
const data = (await response.json()) as { antigravityAckBypass?: boolean };
setAgyAckBypass(data.antigravityAckBypass === true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setAgyAckBypassLoading(false);
}
}, []);
// Load on mount
useEffect(() => {
fetchTokens();
fetchAgyAckBypass();
fetchRawConfig();
}, [fetchTokens, fetchRawConfig]);
}, [fetchTokens, fetchAgyAckBypass, fetchRawConfig]);
// Clear success after timeout
useEffect(() => {
@@ -197,6 +219,41 @@ export default function AuthSection() {
setTimeout(() => setCopiedSecret(false), 2000);
};
const saveAgyAckBypass = async (nextValue: boolean) => {
if (nextValue) {
const confirmed = window.confirm(
'Enable Antigravity power user mode?\n\nThis disables AGY responsibility checklist prompts in CLI and dashboard. You accept full responsibility for OAuth/account risk.'
);
if (!confirmed) return;
}
try {
setAgyAckBypassSaving(true);
setError(null);
const response = await fetch('/api/settings/auth/antigravity-risk', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ antigravityAckBypass: nextValue }),
});
if (!response.ok) {
const data = (await response.json()) as { error?: string };
throw new Error(data.error || 'Failed to update Antigravity power user mode');
}
setAgyAckBypass(nextValue);
setSuccess(
nextValue ? 'Antigravity power user mode enabled.' : 'Antigravity power user mode disabled.'
);
await fetchRawConfig();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setAgyAckBypassSaving(false);
}
};
if (loading || !tokens) {
return (
<div className="flex-1 flex items-center justify-center">
@@ -360,6 +417,29 @@ export default function AuthSection() {
</div>
{/* Actions */}
<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">
<div className="flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-amber-700 dark:text-amber-300" />
<h3 className="text-base font-medium">Antigravity Power User Mode</h3>
</div>
<p className="text-xs text-muted-foreground">
Skip AGY responsibility checklists in Add Account and `ccs agy` flows.
</p>
</div>
<Switch
checked={agyAckBypass}
disabled={agyAckBypassLoading || agyAckBypassSaving || saving}
onCheckedChange={saveAgyAckBypass}
/>
</div>
<p className="text-xs text-amber-800/90 dark:text-amber-200/90">
Use only if you fully understand the OAuth suspension/ban risk pattern (#622). CCS
cannot assume responsibility for account loss.
</p>
</div>
<div className="pt-4 border-t">
<Button
variant="outline"
@@ -385,6 +465,7 @@ export default function AuthSection() {
size="sm"
onClick={() => {
fetchTokens();
fetchAgyAckBypass();
fetchRawConfig();
}}
disabled={loading || saving}