refactor(cliproxy/executor): extract auth-coordinator from index.ts

Phase 06 of #1162. Splits the largest remaining concern out of the
orchestrator into a focused module:

- src/cliproxy/executor/auth-coordinator.ts (397 LOC):
  handleLogout, handleImport, resolveSkipLocalAuth, runAntigravityGate,
  ensureProviderAuthentication, runPreflightQuotaCheck,
  runAccountSafetyGuards, ensureModelConfiguration,
  ensureProviderSettingsFile. Preserves load-bearing ordering of
  antigravity gate -> auth -> token refresh -> quota check.
- 36 unit tests covering --auth/--logout/--import early exit,
  antigravity gate refusal/acceptance, OAuth trigger paths, composite
  providers, remote-proxy skipLocalAuth.

index.ts: 895 -> 716 LOC (-179). Behavior unchanged; full suite passes
1824/1824. Module is 397 LOC (over the <200 ideal) — kept whole because
the auth ordering contract should not be split across files.

Refs #1162
This commit is contained in:
Tam Nhu Tran
2026-05-02 22:03:46 -04:00
parent bc48613bbd
commit 8b7e7f4847
3 changed files with 924 additions and 219 deletions
@@ -0,0 +1,487 @@
/**
* Auth Coordinator Tests (Phase 06)
*
* Tests for auth-coordinator.ts exported functions.
*
* Strategy:
* - jest.mock() at top level to intercept all imports (static + dynamic)
* before the module-under-test is loaded
* - process.exit spied in beforeEach to prevent runner exit
* - Dynamic-import paths (e.g. '../auth/auth-handler') are mocked via
* jest.mock() from the coordinator's perspective (relative to executor/)
*/
import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test';
import type { ExecutorConfig } from '../../types';
import type { UnifiedConfig } from '../../../config/schemas/unified-config';
import type { AuthCoordinationContext } from '../auth-coordinator';
import type { ParsedExecutorFlags } from '../arg-parser';
// ── Module mocks (must be top-level, before any import of subject) ────────────
const mockIsAuthenticated = jest.fn(() => false);
const mockClearAuth = jest.fn(() => true);
const mockTriggerOAuth = jest.fn(async () => true);
jest.mock('../../auth/auth-handler', () => ({
isAuthenticated: (...args: unknown[]) => mockIsAuthenticated(...args),
clearAuth: (...args: unknown[]) => mockClearAuth(...args),
triggerOAuth: (...args: unknown[]) => mockTriggerOAuth(...args),
}));
const mockEnsureAntigravity = jest.fn(async () => true);
const MOCK_ANTIGRAVITY_FLAGS = ['--accept-agr-risk', '--accept-antigravity-risk'];
jest.mock('../../auth/antigravity-responsibility', () => ({
ensureCliAntigravityResponsibility: (...args: unknown[]) => mockEnsureAntigravity(...args),
ANTIGRAVITY_ACCEPT_RISK_FLAGS: MOCK_ANTIGRAVITY_FLAGS,
}));
const mockHandleTokenExpiration = jest.fn(async () => {});
const mockHandleQuotaCheck = jest.fn(async () => {});
jest.mock('../retry-handler', () => ({
handleTokenExpiration: (...args: unknown[]) => mockHandleTokenExpiration(...args),
handleQuotaCheck: (...args: unknown[]) => mockHandleQuotaCheck(...args),
}));
const mockApplyAccountSafetyGuards = jest.fn(() => {});
const mockTouchDefaultAccount = jest.fn(() => {});
jest.mock('../account-resolution', () => ({
applyAccountSafetyGuards: (...args: unknown[]) => mockApplyAccountSafetyGuards(...args),
touchDefaultAccount: (...args: unknown[]) => mockTouchDefaultAccount(...args),
}));
const mockConfigureProviderModel = jest.fn(async () => {});
const mockGetCurrentModel = jest.fn(() => 'claude-opus');
jest.mock('../../config/model-config', () => ({
configureProviderModel: (...args: unknown[]) => mockConfigureProviderModel(...args),
getCurrentModel: (...args: unknown[]) => mockGetCurrentModel(...args),
}));
const mockReconcileCodexModel = jest.fn(async () => {});
jest.mock('../../ai-providers/codex-plan-compatibility', () => ({
reconcileCodexModelForActivePlan: (...args: unknown[]) => mockReconcileCodexModel(...args),
}));
// Do NOT mock quota-manager, model-catalog, config-generator — pure modules
// with no I/O side effects; mocking them would strip exports needed by the
// wider module graph and cause "Export not found" errors.
// ── Import subject AFTER mocks ─────────────────────────────────────────────────
const {
resolveSkipLocalAuth,
handleLogout,
handleImport,
runAntigravityGate,
ensureProviderAuthentication,
runPreflightQuotaCheck,
runAccountSafetyGuards,
ensureModelConfiguration,
} = await import('../auth-coordinator');
// ── Helpers ───────────────────────────────────────────────────────────────────
function makeFlags(overrides: Partial<Record<string, unknown>> = {}): ParsedExecutorFlags {
return {
forceAuth: false,
pasteCallback: false,
portForward: false,
forceHeadless: false,
forceLogout: false,
forceConfig: false,
addAccount: false,
showAccounts: false,
forceImport: false,
gitlabTokenLogin: false,
acceptAgyRisk: false,
incognitoFlag: false,
noIncognitoFlag: false,
noIncognito: false,
useAccount: undefined,
setNickname: undefined,
kiroAuthMethod: undefined,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: {
value: undefined,
sourceFlag: undefined,
sourceDisplay: 'none',
duplicateDisplays: [],
} as ParsedExecutorFlags['thinkingParse'],
...overrides,
} as unknown as ParsedExecutorFlags;
}
function makeCtx(overrides: Partial<AuthCoordinationContext> = {}): AuthCoordinationContext {
return {
provider: 'gemini',
compositeProviders: [],
parsedFlags: makeFlags(),
cfg: { port: 8090, timeout: 5000, verbose: false, pollInterval: 100 } as ExecutorConfig,
unifiedConfig: {} as UnifiedConfig,
verbose: false,
log: () => {},
...overrides,
};
}
// ── resolveSkipLocalAuth ──────────────────────────────────────────────────────
describe('resolveSkipLocalAuth', () => {
it('returns false when useRemoteProxy=false', () => {
expect(resolveSkipLocalAuth('tok', false)).toBe(false);
});
it('returns false when token is whitespace only', () => {
expect(resolveSkipLocalAuth(' ', true)).toBe(false);
});
it('returns false when token is undefined', () => {
expect(resolveSkipLocalAuth(undefined, true)).toBe(false);
});
it('returns true when useRemoteProxy=true and token non-empty', () => {
expect(resolveSkipLocalAuth('tok123', true)).toBe(true);
});
});
// ── handleLogout ──────────────────────────────────────────────────────────────
describe('handleLogout', () => {
let exitSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
jest.clearAllMocks();
exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as typeof process.exit);
});
afterEach(() => {
exitSpy.mockRestore();
});
it('returns false and does not exit when forceLogout=false', async () => {
const result = await handleLogout(makeCtx({ parsedFlags: makeFlags({ forceLogout: false }) }));
expect(result).toBe(false);
expect(exitSpy).not.toHaveBeenCalled();
});
it('exits 0 when forceLogout=true and clearAuth succeeds', async () => {
mockClearAuth.mockReturnValue(true);
await handleLogout(makeCtx({ parsedFlags: makeFlags({ forceLogout: true }) }));
expect(exitSpy).toHaveBeenCalledWith(0);
});
it('exits 0 even when no auth found (clearAuth returns false)', async () => {
mockClearAuth.mockReturnValue(false);
await handleLogout(makeCtx({ parsedFlags: makeFlags({ forceLogout: true }) }));
expect(exitSpy).toHaveBeenCalledWith(0);
});
});
// ── handleImport ──────────────────────────────────────────────────────────────
describe('handleImport', () => {
let exitSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
jest.clearAllMocks();
mockTriggerOAuth.mockResolvedValue(true);
exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as typeof process.exit);
});
afterEach(() => {
exitSpy.mockRestore();
});
it('returns false when forceImport=false', async () => {
const result = await handleImport(makeCtx({ parsedFlags: makeFlags({ forceImport: false }) }));
expect(result).toBe(false);
expect(exitSpy).not.toHaveBeenCalled();
});
it('exits 1 for non-kiro provider', async () => {
await handleImport(
makeCtx({ provider: 'gemini', parsedFlags: makeFlags({ forceImport: true }) })
);
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('exits 1 when --import + --auth combined', async () => {
await handleImport(
makeCtx({ provider: 'kiro', parsedFlags: makeFlags({ forceImport: true, forceAuth: true }) })
);
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('exits 1 when --import + --logout combined', async () => {
await handleImport(
makeCtx({
provider: 'kiro',
parsedFlags: makeFlags({ forceImport: true, forceLogout: true }),
})
);
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('exits 0 on successful kiro import', async () => {
mockTriggerOAuth.mockResolvedValue(true);
await handleImport(
makeCtx({ provider: 'kiro', parsedFlags: makeFlags({ forceImport: true }) })
);
expect(exitSpy).toHaveBeenCalledWith(0);
});
it('exits 1 when triggerOAuth returns false', async () => {
mockTriggerOAuth.mockResolvedValue(false);
await handleImport(
makeCtx({ provider: 'kiro', parsedFlags: makeFlags({ forceImport: true }) })
);
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
// ── runAntigravityGate ────────────────────────────────────────────────────────
describe('runAntigravityGate', () => {
let exitSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
jest.clearAllMocks();
mockEnsureAntigravity.mockResolvedValue(true);
mockIsAuthenticated.mockReturnValue(false);
exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as typeof process.exit);
});
afterEach(() => {
exitSpy.mockRestore();
});
it('returns earlyReturn=false for non-agy provider without calling gate', async () => {
const result = await runAntigravityGate(makeCtx({ provider: 'gemini' }), false);
expect(result.earlyReturn).toBe(false);
expect(mockEnsureAntigravity).not.toHaveBeenCalled();
});
it('agy + forceAuth + skipLocalAuth + acknowledged → earlyReturn=true', async () => {
mockEnsureAntigravity.mockResolvedValue(true);
const ctx = makeCtx({ provider: 'agy', parsedFlags: makeFlags({ forceAuth: true }) });
const result = await runAntigravityGate(ctx, true);
expect(result.earlyReturn).toBe(true);
expect(mockEnsureAntigravity).toHaveBeenCalledWith(
expect.objectContaining({ context: 'oauth' })
);
});
it('agy + forceAuth + skipLocalAuth + refused → throws', async () => {
mockEnsureAntigravity.mockResolvedValue(false);
const ctx = makeCtx({ provider: 'agy', parsedFlags: makeFlags({ forceAuth: true }) });
await expect(runAntigravityGate(ctx, true)).rejects.toThrow('Antigravity auth blocked');
});
it('agy + no forceAuth + skipLocalAuth + acknowledged → earlyReturn=false, no exit', async () => {
mockEnsureAntigravity.mockResolvedValue(true);
mockIsAuthenticated.mockReturnValue(true); // already authenticated → run context triggers
const ctx = makeCtx({ provider: 'agy', parsedFlags: makeFlags({ forceAuth: false }) });
const result = await runAntigravityGate(ctx, true);
expect(result.earlyReturn).toBe(false);
expect(exitSpy).not.toHaveBeenCalled();
expect(mockEnsureAntigravity).toHaveBeenCalledWith(expect.objectContaining({ context: 'run' }));
});
it('agy + no forceAuth + skipLocalAuth + refused → process.exit(1)', async () => {
mockEnsureAntigravity.mockResolvedValue(false);
mockIsAuthenticated.mockReturnValue(true);
const ctx = makeCtx({ provider: 'agy', parsedFlags: makeFlags({ forceAuth: false }) });
await runAntigravityGate(ctx, true);
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
// ── ensureProviderAuthentication ──────────────────────────────────────────────
describe('ensureProviderAuthentication', () => {
let exitSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
jest.clearAllMocks();
mockIsAuthenticated.mockReturnValue(false);
mockTriggerOAuth.mockResolvedValue(true);
mockHandleTokenExpiration.mockResolvedValue(undefined);
mockTouchDefaultAccount.mockImplementation(() => {});
exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as typeof process.exit);
});
afterEach(() => {
exitSpy.mockRestore();
});
it('already authenticated → no OAuth trigger, no exit', async () => {
mockIsAuthenticated.mockReturnValue(true);
await ensureProviderAuthentication(makeCtx({ provider: 'gemini' }));
expect(mockTriggerOAuth).not.toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
});
it('not authenticated → triggerOAuth called', async () => {
mockIsAuthenticated.mockReturnValue(false);
await ensureProviderAuthentication(makeCtx({ provider: 'gemini' }));
expect(mockTriggerOAuth).toHaveBeenCalledWith('gemini', expect.any(Object));
});
it('forceAuth=true → exits 0 after successful OAuth', async () => {
await ensureProviderAuthentication(
makeCtx({ provider: 'gemini', parsedFlags: makeFlags({ forceAuth: true }) })
);
expect(exitSpy).toHaveBeenCalledWith(0);
});
it('OAuth fails → throws Authentication required error', async () => {
mockTriggerOAuth.mockResolvedValue(false);
await expect(ensureProviderAuthentication(makeCtx({ provider: 'gemini' }))).rejects.toThrow(
'Authentication required'
);
});
it('calls touchDefaultAccount after successful single-provider auth', async () => {
mockIsAuthenticated.mockReturnValue(true);
await ensureProviderAuthentication(makeCtx({ provider: 'gemini' }));
expect(mockTouchDefaultAccount).toHaveBeenCalledWith('gemini');
});
it('runs token refresh after single-provider auth', async () => {
mockIsAuthenticated.mockReturnValue(true);
await ensureProviderAuthentication(makeCtx({ provider: 'gemini' }));
expect(mockHandleTokenExpiration).toHaveBeenCalledWith('gemini', false);
});
it('composite + forceAuth: all succeed → exit(0)', async () => {
mockTriggerOAuth.mockResolvedValue(true);
const ctx = makeCtx({
provider: 'agy',
compositeProviders: ['gemini', 'codex'],
parsedFlags: makeFlags({ forceAuth: true }),
});
await ensureProviderAuthentication(ctx);
expect(exitSpy).toHaveBeenCalledWith(0);
expect(mockTriggerOAuth).toHaveBeenCalledTimes(2);
});
it('composite + forceAuth: partial failure → exit(1)', async () => {
let call = 0;
mockTriggerOAuth.mockImplementation(async () => ++call === 1);
const ctx = makeCtx({
provider: 'agy',
compositeProviders: ['gemini', 'codex'],
parsedFlags: makeFlags({ forceAuth: true }),
});
await ensureProviderAuthentication(ctx);
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('composite no forceAuth + unauthenticated → exit(1)', async () => {
mockIsAuthenticated.mockReturnValue(false);
const ctx = makeCtx({
provider: 'agy',
compositeProviders: ['gemini', 'codex'],
parsedFlags: makeFlags({ forceAuth: false }),
});
await ensureProviderAuthentication(ctx);
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('composite all authenticated → token refresh for each provider', async () => {
mockIsAuthenticated.mockReturnValue(true);
const ctx = makeCtx({
provider: 'agy',
compositeProviders: ['gemini', 'codex'],
parsedFlags: makeFlags({ forceAuth: false }),
});
await ensureProviderAuthentication(ctx);
expect(exitSpy).not.toHaveBeenCalled();
expect(mockHandleTokenExpiration).toHaveBeenCalledTimes(2);
});
});
// ── runPreflightQuotaCheck ────────────────────────────────────────────────────
describe('runPreflightQuotaCheck', () => {
beforeEach(() => jest.clearAllMocks());
it('single provider → handleQuotaCheck called once with that provider', async () => {
await runPreflightQuotaCheck('gemini', []);
expect(mockHandleQuotaCheck).toHaveBeenCalledWith('gemini');
expect(mockHandleQuotaCheck).toHaveBeenCalledTimes(1);
});
it('composite list → checks only managed providers from the list', async () => {
// Real MANAGED_QUOTA_PROVIDERS = ['agy','claude','codex','gemini','ghcp']
// 'gemini' and 'codex' are both managed; 'kiro' is not
await runPreflightQuotaCheck('agy', ['gemini', 'kiro']);
expect(mockHandleQuotaCheck).toHaveBeenCalledWith('gemini');
expect(mockHandleQuotaCheck).not.toHaveBeenCalledWith('kiro');
});
});
// ── runAccountSafetyGuards ────────────────────────────────────────────────────
describe('runAccountSafetyGuards', () => {
beforeEach(() => jest.clearAllMocks());
it('delegates to applyAccountSafetyGuards with correct args', () => {
runAccountSafetyGuards('gemini', ['codex']);
expect(mockApplyAccountSafetyGuards).toHaveBeenCalledWith('gemini', ['codex']);
});
});
// ── ensureModelConfiguration ──────────────────────────────────────────────────
describe('ensureModelConfiguration', () => {
beforeEach(() => jest.clearAllMocks());
it('non-composite provider with model support → configureProviderModel called', async () => {
const cfg = { isComposite: false, customSettingsPath: undefined } as ExecutorConfig;
await ensureModelConfiguration('gemini', cfg, false);
expect(mockConfigureProviderModel).toHaveBeenCalledWith('gemini', false, undefined);
});
it('composite variant → configureProviderModel NOT called', async () => {
const cfg = { isComposite: true } as ExecutorConfig;
await ensureModelConfiguration('gemini', cfg, false);
expect(mockConfigureProviderModel).not.toHaveBeenCalled();
});
it('agy (has model support) → configureProviderModel IS called', async () => {
// agy IS in MODEL_CATALOG so supportsModelConfig returns true
const cfg = { isComposite: false } as ExecutorConfig;
await ensureModelConfiguration('agy', cfg, false);
expect(mockConfigureProviderModel).toHaveBeenCalled();
});
it('codex non-composite → reconcileCodexModelForActivePlan called', async () => {
const cfg = { isComposite: false } as ExecutorConfig;
await ensureModelConfiguration('codex', cfg, false);
expect(mockReconcileCodexModel).toHaveBeenCalled();
});
it('codex composite → reconcileCodexModelForActivePlan NOT called', async () => {
const cfg = { isComposite: true } as ExecutorConfig;
await ensureModelConfiguration('codex', cfg, false);
expect(mockReconcileCodexModel).not.toHaveBeenCalled();
});
});
+397
View File
@@ -0,0 +1,397 @@
/**
* Auth Coordinator — Executor-level authentication handoff
*
* Extracted from executor/index.ts (Phase 06).
* Handles:
* - --logout early exit
* - --import token early exit (Kiro only)
* - --auth / forceAuth OAuth flow (single + composite providers)
* - Antigravity responsibility gate (run and oauth contexts)
* - isAuthenticated checks + automatic OAuth trigger
* - Proactive token refresh (multi-provider for composite)
* - lastUsedAt touch via touchDefaultAccount
* - Preflight quota check
* - Account safety guards (cross-provider isolation)
* - First-run model configuration
*
* ORDERING (load-bearing — do not change):
* 1. logout/import/forceAuth early exits
* 2. Remote-proxy auth skip detection
* 3. Antigravity gate (oauth context: remote+forceAuth | run context: else branch)
* 4. OAuth check / trigger (single or composite)
* 5. Token refresh
* 6. lastUsedAt touch
* 7. Preflight quota check
* 8. Account safety guards
* 9. First-run model configuration
*/
import { ok, fail, info } from '../../utils/ui';
import { isAuthenticated } from '../auth/auth-handler';
import { CLIProxyProvider, ExecutorConfig } from '../types';
import { getProviderConfig, ensureProviderSettings } from '../config/config-generator';
import { configureProviderModel, getCurrentModel } from '../config/model-config';
import { reconcileCodexModelForActivePlan } from '../ai-providers/codex-plan-compatibility';
import { supportsModelConfig } from '../model-catalog';
import {
ensureCliAntigravityResponsibility,
ANTIGRAVITY_ACCEPT_RISK_FLAGS,
} from '../auth/antigravity-responsibility';
import { handleTokenExpiration, handleQuotaCheck } from './retry-handler';
import { applyAccountSafetyGuards, touchDefaultAccount } from './account-resolution';
import { MANAGED_QUOTA_PROVIDERS } from '../quota/quota-manager';
import type { ParsedExecutorFlags } from './arg-parser';
import type { UnifiedConfig } from '../../config/schemas/unified-config';
// ── Context / Result types ─────────────────────────────────────────────────────
export interface AuthCoordinationContext {
provider: CLIProxyProvider;
compositeProviders: CLIProxyProvider[];
parsedFlags: ParsedExecutorFlags;
cfg: ExecutorConfig;
unifiedConfig: UnifiedConfig;
verbose: boolean;
log: (msg: string) => void;
}
// ── 1. Special early-exit auth flags (logout / import) ────────────────────────
/**
* Handle --logout: clear auth and exit 0.
* Returns true if early exit occurred (caller should return immediately).
*/
export async function handleLogout(context: AuthCoordinationContext): Promise<boolean> {
const { provider, parsedFlags } = context;
if (!parsedFlags.forceLogout) return false;
const providerConfig = getProviderConfig(provider);
const { clearAuth } = await import('../auth/auth-handler');
if (clearAuth(provider)) {
console.log(ok(`Logged out from ${providerConfig.displayName}`));
} else {
console.log(info(`No authentication found for ${providerConfig.displayName}`));
}
process.exit(0);
}
/**
* Handle --import: Kiro-only token import flow, exits when done.
* Returns true if early exit occurred.
*/
export async function handleImport(context: AuthCoordinationContext): Promise<boolean> {
const { provider, parsedFlags, verbose } = context;
const {
forceImport,
forceAuth,
forceLogout,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
setNickname,
} = parsedFlags;
if (!forceImport) return false;
if (provider !== 'kiro') {
console.error(fail('--import is only available for Kiro'));
console.error(` Run "ccs ${provider} --auth" to authenticate`);
process.exit(1);
}
if (forceAuth) {
console.error(fail('Cannot use --import with --auth'));
console.error(' --import: Import existing token from Kiro IDE');
console.error(' --auth: Trigger new OAuth flow in browser');
process.exit(1);
}
if (forceLogout) {
console.error(fail('Cannot use --import with --logout'));
process.exit(1);
}
const { triggerOAuth } = await import('../auth/auth-handler');
const authSuccess = await triggerOAuth(provider, {
verbose,
import: true,
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
...(setNickname ? { nickname: setNickname } : {}),
});
if (!authSuccess) {
console.error(fail('Failed to import Kiro token from IDE'));
console.error(' Make sure you are logged into Kiro IDE first');
process.exit(1);
}
process.exit(0);
}
// ── 2. Remote proxy auth skip ──────────────────────────────────────────────────
/**
* Returns true when a remote proxy with an authToken is active —
* local OAuth is not needed in this case.
*/
export function resolveSkipLocalAuth(
remoteAuthToken: string | undefined,
useRemoteProxy: boolean
): boolean {
return useRemoteProxy && !!remoteAuthToken?.trim();
}
// ── 3. Antigravity responsibility gate ────────────────────────────────────────
/**
* Runs the Antigravity responsibility gate for the relevant code path.
*
* Two scenarios (must match original ordering in index.ts):
* A. provider=agy + forceAuth + skipLocalAuth → oauth-context gate; if refused, log and return
* (caller must return early — this function returns { earlyReturn: true })
* B. provider=agy + !forceAuth + (skipLocalAuth || !requiresAuthNow) → run-context gate;
* if refused, log and process.exit(1)
*
* Returns { earlyReturn: true } when the caller should return immediately (case A refused).
*/
export async function runAntigravityGate(
context: AuthCoordinationContext,
skipLocalAuth: boolean
): Promise<{ earlyReturn: boolean }> {
const { provider, parsedFlags } = context;
const { forceAuth, acceptAgyRisk } = parsedFlags;
if (provider !== 'agy') return { earlyReturn: false };
const providerConfig = getProviderConfig(provider);
const requiresAuthNow = providerConfig.requiresOAuth && !isAuthenticated(provider);
if (forceAuth && skipLocalAuth) {
// Case A: remote proxy + forceAuth — gate in oauth context, skip local OAuth
const acknowledged = await ensureCliAntigravityResponsibility({
context: 'oauth',
acceptedByFlag: acceptAgyRisk,
});
if (!acknowledged) {
throw new Error(
`Antigravity auth blocked. Re-run after completing confirmation or pass ${ANTIGRAVITY_ACCEPT_RISK_FLAGS[0]}.`
);
}
console.error(info('Remote proxy mode is active; local OAuth flow is skipped in --auth mode.'));
return { earlyReturn: true };
}
if (!forceAuth) {
// Case B: run-context gate (only when auth not immediately required)
if (skipLocalAuth || !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);
}
}
}
return { earlyReturn: false };
}
// ── 46. OAuth check / trigger + token refresh + account touch ─────────────────
/**
* Ensure provider(s) are authenticated; trigger OAuth if needed.
* Handles both composite (multi-provider) and single-provider flows.
* Also runs proactive token refresh and touches lastUsedAt.
*
* Only called when providerConfig.requiresOAuth && !skipLocalAuth.
*/
export async function ensureProviderAuthentication(
context: AuthCoordinationContext
): Promise<void> {
const { provider, compositeProviders, parsedFlags, verbose, log } = context;
const {
forceAuth,
addAccount,
acceptAgyRisk,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabTokenLogin,
gitlabBaseUrl,
forceHeadless,
setNickname,
noIncognito,
pasteCallback,
portForward,
} = parsedFlags;
log(`Checking authentication for ${provider}`);
// Multi-provider path (composite variants)
if (compositeProviders.length > 0) {
if (forceAuth) {
const { triggerOAuth } = await import('../auth/auth-handler');
const failures: string[] = [];
for (const p of compositeProviders) {
const authSuccess = await triggerOAuth(p, {
verbose,
add: addAccount,
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
...(gitlabTokenLogin && p === 'gitlab' ? { gitlabAuthMode: 'pat' as const } : {}),
...(gitlabBaseUrl && p === 'gitlab' ? { gitlabBaseUrl } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
...(noIncognito ? { noIncognito: true } : {}),
...(pasteCallback ? { pasteCallback: true } : {}),
...(portForward ? { portForward: true } : {}),
});
if (!authSuccess) {
failures.push(p);
}
}
if (failures.length > 0) {
const succeeded = compositeProviders.filter((p) => !failures.includes(p));
console.error(fail(`Auth failed for: ${failures.join(', ')}`));
if (succeeded.length > 0) {
console.error(info(`Succeeded: ${succeeded.join(', ')}`));
}
process.exit(1);
}
process.exit(0);
}
// Check for unauthenticated providers
const unauthenticatedProviders: string[] = [];
for (const p of compositeProviders) {
if (!isAuthenticated(p)) {
unauthenticatedProviders.push(p);
}
}
if (unauthenticatedProviders.length > 0) {
console.error(fail('Composite variant requires authentication for multiple providers:'));
for (const p of unauthenticatedProviders) {
console.error(` - ${p} (run "ccs ${p} --auth")`);
}
process.exit(1);
}
} else {
// Single-provider path
if (forceAuth || !isAuthenticated(provider)) {
const { triggerOAuth } = await import('../auth/auth-handler');
const providerConfig = getProviderConfig(provider);
const authSuccess = await triggerOAuth(provider, {
verbose,
add: addAccount,
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
...(gitlabTokenLogin ? { gitlabAuthMode: 'pat' as const } : {}),
...(gitlabBaseUrl ? { gitlabBaseUrl } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
...(noIncognito ? { noIncognito: true } : {}),
...(pasteCallback ? { pasteCallback: true } : {}),
...(portForward ? { portForward: true } : {}),
});
if (!authSuccess) {
throw new Error(`Authentication required for ${providerConfig.displayName}`);
}
if (forceAuth) {
process.exit(0);
}
} else {
log(`${provider} already authenticated`);
}
}
// 3a. Proactive token refresh (multi-provider for composite)
if (compositeProviders.length > 0) {
for (const p of compositeProviders) {
await handleTokenExpiration(p, verbose);
}
} else {
await handleTokenExpiration(provider, verbose);
}
// 3a-1. Update lastUsedAt
touchDefaultAccount(provider);
}
// ── 7. Preflight quota check ──────────────────────────────────────────────────
/**
* Preflight quota check for providers with quota-based rotation.
* Only runs when !skipLocalAuth.
*/
export async function runPreflightQuotaCheck(
provider: CLIProxyProvider,
compositeProviders: CLIProxyProvider[]
): Promise<void> {
if (compositeProviders.length > 0) {
for (const managedProvider of MANAGED_QUOTA_PROVIDERS) {
if (compositeProviders.includes(managedProvider)) {
await handleQuotaCheck(managedProvider);
}
}
} else {
await handleQuotaCheck(provider);
}
}
// ── 8. Account safety guards ──────────────────────────────────────────────────
/**
* Enforce cross-provider account isolation. Only runs when !skipLocalAuth.
*/
export function runAccountSafetyGuards(
provider: CLIProxyProvider,
compositeProviders: CLIProxyProvider[]
): void {
applyAccountSafetyGuards(provider, compositeProviders);
}
// ── 9. First-run model configuration ─────────────────────────────────────────
/**
* Ensure provider model is configured on first run.
* Skipped for composite variants and remote proxy mode.
* Also reconciles Codex model for active plan.
*/
export async function ensureModelConfiguration(
provider: CLIProxyProvider,
cfg: ExecutorConfig,
verbose: boolean
): Promise<void> {
if (!cfg.isComposite && supportsModelConfig(provider)) {
await configureProviderModel(provider, false, cfg.customSettingsPath);
}
if (provider === 'codex' && !cfg.isComposite) {
await reconcileCodexModelForActivePlan({
currentModel: getCurrentModel(provider, cfg.customSettingsPath),
verbose,
});
}
}
// ── Ensure provider settings file ────────────────────────────────────────────
/**
* Ensure the provider settings file exists.
*/
export function ensureProviderSettingsFile(provider: CLIProxyProvider): void {
ensureProviderSettings(provider);
}
+40 -219
View File
@@ -14,22 +14,19 @@ import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ok, fail, info, warn } from '../../utils/ui';
import { fail, info, warn } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import { escapeShellArg, getWindowsEscapedCommandShell } from '../../utils/shell-executor';
import {
generateConfig,
getProviderConfig,
ensureProviderSettings,
getProviderSettingsPath,
CLIPROXY_DEFAULT_PORT,
} from '../config/config-generator';
import { isAuthenticated } from '../auth/auth-handler';
import { CLIProxyProvider, ExecutorConfig } from '../types';
import { configureProviderModel, getCurrentModel } from '../config/model-config';
import { reconcileCodexModelForActivePlan } from '../ai-providers/codex-plan-compatibility';
import { supportsModelConfig } from '../model-catalog';
import { CLIProxyProvider, ExecutorConfig } from '../types';
import {
supportsModelConfig,
isModelBroken,
getModelIssueUrl,
findModel,
@@ -58,9 +55,7 @@ import { resolveProfileContinuityInheritance } from '../../auth/profile-continui
import { resolveBrowserLaunchFlags, resolveBrowserRuntime } from './browser-launch-setup';
import {
resolveRuntimeQuotaMonitorProviders as _resolveRuntimeQuotaMonitorProviders,
applyAccountSafetyGuards,
resolveAccounts,
touchDefaultAccount,
} from './account-resolution';
import { waitForProxyReadyWithSpinner, spawnProxy } from './lifecycle-manager';
import {
@@ -68,14 +63,19 @@ import {
logEnvironment,
resolveCliproxyImageAnalysisEnv,
} from './env-resolver';
import { handleTokenExpiration, handleQuotaCheck } from './retry-handler';
import { MANAGED_QUOTA_PROVIDERS } from '../quota/quota-manager';
import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge';
import {
ensureCliAntigravityResponsibility,
ANTIGRAVITY_ACCEPT_RISK_FLAGS,
} from '../auth/antigravity-responsibility';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import {
handleLogout,
handleImport,
resolveSkipLocalAuth,
runAntigravityGate,
ensureProviderAuthentication,
runPreflightQuotaCheck,
runAccountSafetyGuards,
ensureModelConfiguration,
ensureProviderSettingsFile,
} from './auth-coordinator';
import {
buildThinkingStartupStatus,
resolveRuntimeThinkingOverride,
@@ -182,25 +182,11 @@ export async function execClaudeWithCLIProxy(
if (process.exitCode === 1) return;
const {
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabBaseUrl,
extendedContextOverride,
thinkingParse,
} = parsedFlags;
@@ -244,209 +230,51 @@ export async function execClaudeWithCLIProxy(
}
}
// Handle --logout
if (forceLogout) {
const { clearAuth } = await import('../auth/auth-handler');
if (clearAuth(provider)) {
console.log(ok(`Logged out from ${providerConfig.displayName}`));
} else {
console.log(info(`No authentication found for ${providerConfig.displayName}`));
}
process.exit(0);
}
// Build auth coordination context (used for logout/import/antigravity/oauth)
const authCtx = {
provider,
compositeProviders,
parsedFlags,
cfg,
unifiedConfig,
verbose,
log,
};
// Handle --import (Kiro only)
if (forceImport) {
if (provider !== 'kiro') {
console.error(fail('--import is only available for Kiro'));
console.error(` Run "ccs ${provider} --auth" to authenticate`);
process.exit(1);
}
if (forceAuth) {
console.error(fail('Cannot use --import with --auth'));
console.error(' --import: Import existing token from Kiro IDE');
console.error(' --auth: Trigger new OAuth flow in browser');
process.exit(1);
}
if (forceLogout) {
console.error(fail('Cannot use --import with --logout'));
process.exit(1);
}
const { triggerOAuth } = await import('../auth/auth-handler');
const authSuccess = await triggerOAuth(provider, {
verbose,
import: true,
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
...(setNickname ? { nickname: setNickname } : {}),
});
if (!authSuccess) {
console.error(fail('Failed to import Kiro token from IDE'));
console.error(' Make sure you are logged into Kiro IDE first');
process.exit(1);
}
process.exit(0);
}
// Handle --logout (early exit)
await handleLogout(authCtx);
// Handle --import (early exit, Kiro only)
await handleImport(authCtx);
// 3. Ensure OAuth completed (if provider requires it)
const remoteAuthToken = proxyConfig.authToken?.trim();
const skipLocalAuth = useRemoteProxy && !!remoteAuthToken;
const skipLocalAuth = resolveSkipLocalAuth(remoteAuthToken, useRemoteProxy);
if (skipLocalAuth) {
log(`Using remote proxy authentication (skipping local OAuth)`);
}
if (provider === 'agy' && forceAuth && skipLocalAuth) {
const acknowledged = await ensureCliAntigravityResponsibility({
context: 'oauth',
acceptedByFlag: acceptAgyRisk,
});
if (!acknowledged) {
throw new Error(
`Antigravity auth blocked. Re-run after completing confirmation or pass ${ANTIGRAVITY_ACCEPT_RISK_FLAGS[0]}.`
);
}
console.error(info('Remote proxy mode is active; local OAuth flow is skipped in --auth mode.'));
return;
}
if (provider === 'agy' && !forceAuth) {
const requiresAuthNow = providerConfig.requiresOAuth && !isAuthenticated(provider);
if (skipLocalAuth || !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);
}
}
}
// Antigravity gate (runs before OAuth check)
const { earlyReturn: agyEarlyReturn } = await runAntigravityGate(authCtx, skipLocalAuth);
if (agyEarlyReturn) return;
if (providerConfig.requiresOAuth && !skipLocalAuth) {
log(`Checking authentication for ${provider}`);
// Multi-provider auth check for composite variants
if (compositeProviders.length > 0) {
// Handle forceAuth for composite providers
if (forceAuth) {
const { triggerOAuth } = await import('../auth/auth-handler');
const failures: string[] = [];
for (const p of compositeProviders) {
const authSuccess = await triggerOAuth(p, {
verbose,
add: addAccount,
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
...(gitlabTokenLogin && p === 'gitlab' ? { gitlabAuthMode: 'pat' as const } : {}),
...(gitlabBaseUrl && p === 'gitlab' ? { gitlabBaseUrl } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
...(noIncognito ? { noIncognito: true } : {}),
...(pasteCallback ? { pasteCallback: true } : {}),
...(portForward ? { portForward: true } : {}),
});
if (!authSuccess) {
failures.push(p);
}
}
if (failures.length > 0) {
const succeeded = compositeProviders.filter((p) => !failures.includes(p));
console.error(fail(`Auth failed for: ${failures.join(', ')}`));
if (succeeded.length > 0) {
console.error(info(`Succeeded: ${succeeded.join(', ')}`));
}
process.exit(1);
}
process.exit(0);
}
// Check for unauthenticated providers
const unauthenticatedProviders: string[] = [];
for (const p of compositeProviders) {
if (!isAuthenticated(p)) {
unauthenticatedProviders.push(p);
}
}
if (unauthenticatedProviders.length > 0) {
console.error(fail('Composite variant requires authentication for multiple providers:'));
for (const p of unauthenticatedProviders) {
console.error(` - ${p} (run "ccs ${p} --auth")`);
}
process.exit(1);
}
} else if (forceAuth || !isAuthenticated(provider)) {
const { triggerOAuth } = await import('../auth/auth-handler');
const authSuccess = await triggerOAuth(provider, {
verbose,
add: addAccount,
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
...(gitlabTokenLogin ? { gitlabAuthMode: 'pat' as const } : {}),
...(gitlabBaseUrl ? { gitlabBaseUrl } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
...(noIncognito ? { noIncognito: true } : {}),
...(pasteCallback ? { pasteCallback: true } : {}),
...(portForward ? { portForward: true } : {}),
});
if (!authSuccess) {
throw new Error(`Authentication required for ${providerConfig.displayName}`);
}
if (forceAuth) {
process.exit(0);
}
} else {
log(`${provider} already authenticated`);
}
// 3a. Proactive token refresh (multi-provider for composite)
if (compositeProviders.length > 0) {
for (const p of compositeProviders) {
await handleTokenExpiration(p, verbose);
}
} else {
await handleTokenExpiration(provider, verbose);
}
// 3a-1. Update lastUsedAt
touchDefaultAccount(provider);
await ensureProviderAuthentication(authCtx);
}
// 3b. Preflight quota check (providers with quota-based rotation)
if (!skipLocalAuth) {
// Multi-tier quota check for composite variants (check if any tier uses a managed provider)
if (compositeProviders.length > 0) {
for (const managedProvider of MANAGED_QUOTA_PROVIDERS) {
if (compositeProviders.includes(managedProvider)) {
await handleQuotaCheck(managedProvider);
}
}
} else {
await handleQuotaCheck(provider);
}
await runPreflightQuotaCheck(provider, compositeProviders);
}
// 3c. Account safety: enforce cross-provider isolation
if (!skipLocalAuth) {
applyAccountSafetyGuards(provider, compositeProviders);
runAccountSafetyGuards(provider, compositeProviders);
}
// 4. First-run model configuration
if (!cfg.isComposite && supportsModelConfig(provider) && !skipLocalAuth) {
await configureProviderModel(provider, false, cfg.customSettingsPath);
// 4. First-run model configuration + codex plan reconcile
if (!skipLocalAuth) {
await ensureModelConfiguration(provider, cfg, verbose);
}
// 5. Check for broken models (multi-tier for composite)
@@ -497,14 +325,7 @@ export async function execClaudeWithCLIProxy(
}
// 6. Ensure user settings file exists
ensureProviderSettings(provider);
if (provider === 'codex' && !cfg.isComposite && !skipLocalAuth) {
await reconcileCodexModelForActivePlan({
currentModel: getCurrentModel(provider, cfg.customSettingsPath),
verbose,
});
}
ensureProviderSettingsFile(provider);
// Local proxy mode: generate config, spawn/join proxy, track session
let proxy: ChildProcess | null = null;