mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-23 16:23:35 +00:00
feat(agy): add power-user bypass for responsibility acknowledgement
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import { createInterface, Interface } from 'readline';
|
import { createInterface, Interface } from 'readline';
|
||||||
import { fail, info, ok, warn } from '../utils/ui';
|
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_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_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';
|
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> {
|
function askQuestion(rl: Interface, prompt: string): Promise<string | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let settled = false;
|
let settled = false;
|
||||||
@@ -160,7 +174,7 @@ export function validateAntigravityRiskAcknowledgement(payload: unknown): Valida
|
|||||||
export async function ensureCliAntigravityResponsibility(
|
export async function ensureCliAntigravityResponsibility(
|
||||||
options: EnsureCliRiskOptions
|
options: EnsureCliRiskOptions
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (options.acceptedByFlag || isTruthyEnv(process.env.CCS_ACCEPT_AGY_RISK)) {
|
if (options.acceptedByFlag || isAntigravityResponsibilityBypassEnabled()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import {
|
|||||||
DEFAULT_CURSOR_CONFIG,
|
DEFAULT_CURSOR_CONFIG,
|
||||||
DEFAULT_GLOBAL_ENV,
|
DEFAULT_GLOBAL_ENV,
|
||||||
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
||||||
|
DEFAULT_CLIPROXY_SAFETY_CONFIG,
|
||||||
DEFAULT_QUOTA_MANAGEMENT_CONFIG,
|
DEFAULT_QUOTA_MANAGEMENT_CONFIG,
|
||||||
DEFAULT_THINKING_CONFIG,
|
DEFAULT_THINKING_CONFIG,
|
||||||
DEFAULT_DASHBOARD_AUTH_CONFIG,
|
DEFAULT_DASHBOARD_AUTH_CONFIG,
|
||||||
DEFAULT_IMAGE_ANALYSIS_CONFIG,
|
DEFAULT_IMAGE_ANALYSIS_CONFIG,
|
||||||
|
CLIProxySafetyConfig,
|
||||||
GlobalEnvConfig,
|
GlobalEnvConfig,
|
||||||
ThinkingConfig,
|
ThinkingConfig,
|
||||||
DashboardAuthConfig,
|
DashboardAuthConfig,
|
||||||
@@ -249,6 +251,11 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
request_log:
|
request_log:
|
||||||
partial.cliproxy?.logging?.request_log ?? defaults.cliproxy.logging?.request_log ?? false,
|
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 config - preserve user values, no defaults (uses constants as fallback)
|
||||||
auth: partial.cliproxy?.auth,
|
auth: partial.cliproxy?.auth,
|
||||||
// Backend selection - validate and preserve user choice (original vs plus)
|
// 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.
|
* Get thinking configuration.
|
||||||
* Returns defaults if not configured.
|
* Returns defaults if not configured.
|
||||||
|
|||||||
@@ -156,6 +156,22 @@ export interface CLIProxyLoggingConfig {
|
|||||||
request_log?: boolean;
|
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.
|
* Token refresh configuration.
|
||||||
* Manages background token refresh worker settings.
|
* Manages background token refresh worker settings.
|
||||||
@@ -187,6 +203,8 @@ export interface CLIProxyConfig {
|
|||||||
variants: Record<string, CLIProxyVariantConfig | CompositeVariantConfig>;
|
variants: Record<string, CLIProxyVariantConfig | CompositeVariantConfig>;
|
||||||
/** Logging configuration (disabled by default) */
|
/** Logging configuration (disabled by default) */
|
||||||
logging?: CLIProxyLoggingConfig;
|
logging?: CLIProxyLoggingConfig;
|
||||||
|
/** Safety controls for high-risk provider flows */
|
||||||
|
safety?: CLIProxySafetyConfig;
|
||||||
/** Kiro: disable incognito browser mode (use normal browser to save credentials) */
|
/** Kiro: disable incognito browser mode (use normal browser to save credentials) */
|
||||||
kiro_no_incognito?: boolean;
|
kiro_no_incognito?: boolean;
|
||||||
/** Global auth configuration for CLIProxyAPI */
|
/** Global auth configuration for CLIProxyAPI */
|
||||||
@@ -777,6 +795,7 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
request_log: false,
|
request_log: false,
|
||||||
},
|
},
|
||||||
|
safety: { ...DEFAULT_CLIPROXY_SAFETY_CONFIG },
|
||||||
auto_sync: true,
|
auto_sync: true,
|
||||||
},
|
},
|
||||||
preferences: {
|
preferences: {
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ 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';
|
import {
|
||||||
|
validateAntigravityRiskAcknowledgement,
|
||||||
|
isAntigravityResponsibilityBypassEnabled,
|
||||||
|
} from '../../cliproxy/antigravity-responsibility';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -406,7 +409,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === 'agy') {
|
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
|
||||||
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
@@ -634,7 +637,7 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === 'agy') {
|
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
|
||||||
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '../../cliproxy';
|
} from '../../cliproxy';
|
||||||
import { regenerateConfig } from '../../cliproxy/config-generator';
|
import { regenerateConfig } from '../../cliproxy/config-generator';
|
||||||
import { deduplicateCcsHooks } from '../../utils/websearch/hook-utils';
|
import { deduplicateCcsHooks } from '../../utils/websearch/hook-utils';
|
||||||
|
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import type { Settings } from '../../types/config';
|
import type { Settings } from '../../types/config';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -383,6 +384,48 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void =>
|
|||||||
|
|
||||||
// ==================== Auth Tokens ====================
|
// ==================== 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)
|
* 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 {
|
import {
|
||||||
ANTIGRAVITY_ACK_PHRASE,
|
ANTIGRAVITY_ACK_PHRASE,
|
||||||
ANTIGRAVITY_ACK_VERSION,
|
ANTIGRAVITY_ACK_VERSION,
|
||||||
hasAntigravityRiskAcceptanceFlag,
|
hasAntigravityRiskAcceptanceFlag,
|
||||||
|
isAntigravityResponsibilityBypassEnabled,
|
||||||
validateAntigravityRiskAcknowledgement,
|
validateAntigravityRiskAcknowledgement,
|
||||||
} from '../../../src/cliproxy/antigravity-responsibility';
|
} from '../../../src/cliproxy/antigravity-responsibility';
|
||||||
|
|
||||||
describe('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', () => {
|
it('accepts a complete acknowledgement payload', () => {
|
||||||
const result = validateAntigravityRiskAcknowledgement({
|
const result = validateAntigravityRiskAcknowledgement({
|
||||||
version: ANTIGRAVITY_ACK_VERSION,
|
version: ANTIGRAVITY_ACK_VERSION,
|
||||||
@@ -75,4 +107,24 @@ describe('antigravity-responsibility', () => {
|
|||||||
expect(hasAntigravityRiskAcceptanceFlag(['--accept-antigravity-risk'])).toBeTrue();
|
expect(hasAntigravityRiskAcceptanceFlag(['--accept-antigravity-risk'])).toBeTrue();
|
||||||
expect(hasAntigravityRiskAcceptanceFlag(['--auth'])).toBeFalse();
|
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,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} 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 { 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';
|
||||||
@@ -68,6 +68,8 @@ export function AddAccountDialog({
|
|||||||
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 [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 [kiroAuthMethod, setKiroAuthMethod] = useState<KiroAuthMethod>(DEFAULT_KIRO_AUTH_METHOD);
|
||||||
const wasAuthenticatingRef = useRef(false);
|
const wasAuthenticatingRef = useRef(false);
|
||||||
const authFlow = useCliproxyAuthFlow();
|
const authFlow = useCliproxyAuthFlow();
|
||||||
@@ -75,7 +77,8 @@ export function AddAccountDialog({
|
|||||||
|
|
||||||
const isKiro = provider === 'kiro';
|
const isKiro = provider === 'kiro';
|
||||||
const requiresSafetyAcknowledgement = provider === 'gemini';
|
const requiresSafetyAcknowledgement = provider === 'gemini';
|
||||||
const requiresAgyResponsibilityFlow = provider === 'agy';
|
const requiresAgyResponsibilityFlow = provider === 'agy' && !agyAckBypassEnabled;
|
||||||
|
const isAgyBypassStatePending = provider === 'agy' && agyAckBypassLoading;
|
||||||
const isAgyRiskChecklistComplete = isAntigravityRiskChecklistComplete(agyRiskChecklist);
|
const isAgyRiskChecklistComplete = isAntigravityRiskChecklistComplete(agyRiskChecklist);
|
||||||
const defaultDeviceCode = isDeviceCodeProvider(provider);
|
const defaultDeviceCode = isDeviceCodeProvider(provider);
|
||||||
const requiresNickname = isNicknameRequiredProvider(provider);
|
const requiresNickname = isNicknameRequiredProvider(provider);
|
||||||
@@ -92,6 +95,8 @@ export function AddAccountDialog({
|
|||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
setAcknowledgedRisk(false);
|
setAcknowledgedRisk(false);
|
||||||
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
setAgyRiskChecklist(DEFAULT_ANTIGRAVITY_RISK_CHECKLIST);
|
||||||
|
setAgyAckBypassEnabled(false);
|
||||||
|
setAgyAckBypassLoading(false);
|
||||||
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
|
||||||
wasAuthenticatingRef.current = false;
|
wasAuthenticatingRef.current = false;
|
||||||
onClose();
|
onClose();
|
||||||
@@ -105,6 +110,44 @@ export function AddAccountDialog({
|
|||||||
}
|
}
|
||||||
}, [provider, open]);
|
}, [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
|
// When authFlow completes successfully (polling detected success), apply preset and close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authFlow.isAuthenticating && !authFlow.error && authFlow.provider === null && open) {
|
if (!authFlow.isAuthenticating && !authFlow.error && authFlow.provider === null && open) {
|
||||||
@@ -153,6 +196,10 @@ export function AddAccountDialog({
|
|||||||
* - Authorization code providers use /start-url and polling.
|
* - Authorization code providers use /start-url and polling.
|
||||||
*/
|
*/
|
||||||
const handleAuthenticate = () => {
|
const handleAuthenticate = () => {
|
||||||
|
if (isAgyBypassStatePending) {
|
||||||
|
setLocalError('Loading Antigravity safety settings. Please wait a moment and retry.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
|
||||||
setLocalError(
|
setLocalError(
|
||||||
'Complete all Antigravity responsibility steps before authenticating this provider.'
|
'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 && (
|
{requiresSafetyAcknowledgement && !showAuthUI && (
|
||||||
<AccountSafetyWarningCard
|
<AccountSafetyWarningCard
|
||||||
provider="gemini"
|
provider="gemini"
|
||||||
@@ -443,6 +501,7 @@ export function AddAccountDialog({
|
|||||||
onClick={handleAuthenticate}
|
onClick={handleAuthenticate}
|
||||||
disabled={
|
disabled={
|
||||||
isPending ||
|
isPending ||
|
||||||
|
isAgyBypassStatePending ||
|
||||||
(requiresNickname && !nicknameTrimmed) ||
|
(requiresNickname && !nicknameTrimmed) ||
|
||||||
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
|
(requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) ||
|
||||||
(requiresSafetyAcknowledgement && !acknowledgedRisk)
|
(requiresSafetyAcknowledgement && !acknowledgedRisk)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
ShieldAlert,
|
||||||
Save,
|
Save,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useRawConfig } from '../hooks';
|
import { useRawConfig } from '../hooks';
|
||||||
@@ -51,6 +53,9 @@ export default function AuthSection() {
|
|||||||
const [editedSecret, setEditedSecret] = useState<string | null>(null);
|
const [editedSecret, setEditedSecret] = useState<string | null>(null);
|
||||||
const [copiedApiKey, setCopiedApiKey] = useState(false);
|
const [copiedApiKey, setCopiedApiKey] = useState(false);
|
||||||
const [copiedSecret, setCopiedSecret] = useState(false);
|
const [copiedSecret, setCopiedSecret] = useState(false);
|
||||||
|
const [agyAckBypass, setAgyAckBypass] = useState(false);
|
||||||
|
const [agyAckBypassLoading, setAgyAckBypassLoading] = useState(true);
|
||||||
|
const [agyAckBypassSaving, setAgyAckBypassSaving] = useState(false);
|
||||||
|
|
||||||
// Fetch tokens
|
// Fetch tokens
|
||||||
const fetchTokens = useCallback(async () => {
|
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
|
// Load on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTokens();
|
fetchTokens();
|
||||||
|
fetchAgyAckBypass();
|
||||||
fetchRawConfig();
|
fetchRawConfig();
|
||||||
}, [fetchTokens, fetchRawConfig]);
|
}, [fetchTokens, fetchAgyAckBypass, fetchRawConfig]);
|
||||||
|
|
||||||
// Clear success after timeout
|
// Clear success after timeout
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -197,6 +219,41 @@ export default function AuthSection() {
|
|||||||
setTimeout(() => setCopiedSecret(false), 2000);
|
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) {
|
if (loading || !tokens) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
@@ -360,6 +417,29 @@ export default function AuthSection() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* 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">
|
<div className="pt-4 border-t">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -385,6 +465,7 @@ export default function AuthSection() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetchTokens();
|
fetchTokens();
|
||||||
|
fetchAgyAckBypass();
|
||||||
fetchRawConfig();
|
fetchRawConfig();
|
||||||
}}
|
}}
|
||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
|
|||||||
Reference in New Issue
Block a user