mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
feat: skip local OAuth when using remote proxy with auth token
When --proxy-host and --proxy-auth-token are provided, the remote proxy handles authentication via its own OAuth sessions. This change skips: - Local OAuth check/trigger - Preflight quota check (managed by remote server) - Model configuration prompts (configured on remote server) - Broken model warnings (model selection is remote) This enables headless CI/CD usage without requiring pre-cached CCS_SESSIONS when a remote proxy with auth token is available.
This commit is contained in:
2 files changed
+239
-14
No files matched your search
@@ -498,7 +498,14 @@ export async function execClaudeWithCLIProxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Ensure OAuth completed (if provider requires it)
|
// 3. Ensure OAuth completed (if provider requires it)
|
||||||
if (providerConfig.requiresOAuth) {
|
// Skip local OAuth check when using remote proxy with auth token
|
||||||
|
// The remote proxy has its own OAuth sessions and handles authentication
|
||||||
|
const skipLocalAuth = useRemoteProxy && proxyConfig.authToken;
|
||||||
|
if (skipLocalAuth) {
|
||||||
|
log(`Using remote proxy authentication (skipping local OAuth)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerConfig.requiresOAuth && !skipLocalAuth) {
|
||||||
log(`Checking authentication for ${provider}`);
|
log(`Checking authentication for ${provider}`);
|
||||||
|
|
||||||
if (forceAuth || !isAuthenticated(provider)) {
|
if (forceAuth || !isAuthenticated(provider)) {
|
||||||
@@ -549,7 +556,8 @@ export async function execClaudeWithCLIProxy(
|
|||||||
|
|
||||||
// 3b. Preflight quota check - auto-switch to account with quota before launch
|
// 3b. Preflight quota check - auto-switch to account with quota before launch
|
||||||
// Uses quota-manager for caching, tier priority, and cooldown support
|
// Uses quota-manager for caching, tier priority, and cooldown support
|
||||||
if (provider === 'agy') {
|
// Skip for remote proxy - quota is managed on the remote server
|
||||||
|
if (provider === 'agy' && !skipLocalAuth) {
|
||||||
const preflight = await preflightCheck(provider);
|
const preflight = await preflightCheck(provider);
|
||||||
|
|
||||||
if (!preflight.proceed) {
|
if (!preflight.proceed) {
|
||||||
@@ -571,23 +579,27 @@ export async function execClaudeWithCLIProxy(
|
|||||||
// 4. First-run model configuration (interactive)
|
// 4. First-run model configuration (interactive)
|
||||||
// For supported providers, prompt user to select model on first run
|
// For supported providers, prompt user to select model on first run
|
||||||
// Pass customSettingsPath for CLIProxy variants
|
// Pass customSettingsPath for CLIProxy variants
|
||||||
if (supportsModelConfig(provider)) {
|
// Skip for remote proxy - model is configured on the remote server
|
||||||
|
if (supportsModelConfig(provider) && !skipLocalAuth) {
|
||||||
await configureProviderModel(provider, false, cfg.customSettingsPath); // false = only if not configured
|
await configureProviderModel(provider, false, cfg.customSettingsPath); // false = only if not configured
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Check for known broken models and warn user
|
// 5. Check for known broken models and warn user
|
||||||
const currentModel = getCurrentModel(provider, cfg.customSettingsPath);
|
// Skip for remote proxy - model selection is on the remote server
|
||||||
if (currentModel && isModelBroken(provider, currentModel)) {
|
if (!skipLocalAuth) {
|
||||||
const modelEntry = findModel(provider, currentModel);
|
const currentModel = getCurrentModel(provider, cfg.customSettingsPath);
|
||||||
const issueUrl = getModelIssueUrl(provider, currentModel);
|
if (currentModel && isModelBroken(provider, currentModel)) {
|
||||||
console.error('');
|
const modelEntry = findModel(provider, currentModel);
|
||||||
console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`));
|
const issueUrl = getModelIssueUrl(provider, currentModel);
|
||||||
console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.');
|
console.error('');
|
||||||
if (issueUrl) {
|
console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`));
|
||||||
console.error(` Tracking: ${issueUrl}`);
|
console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.');
|
||||||
|
if (issueUrl) {
|
||||||
|
console.error(` Tracking: ${issueUrl}`);
|
||||||
|
}
|
||||||
|
console.error(` Run "ccs ${provider} --config" to change model.`);
|
||||||
|
console.error('');
|
||||||
}
|
}
|
||||||
console.error(` Run "ccs ${provider} --config" to change model.`);
|
|
||||||
console.error('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Ensure user settings file exists (creates from defaults if not)
|
// 6. Ensure user settings file exists (creates from defaults if not)
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for skip-local-auth functionality when using remote proxy with auth token
|
||||||
|
*
|
||||||
|
* When --proxy-host and --proxy-auth-token are provided together, the system should
|
||||||
|
* skip local OAuth checks because the remote proxy handles authentication.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
|
||||||
|
describe('skip-local-auth logic', () => {
|
||||||
|
describe('skipLocalAuth flag determination', () => {
|
||||||
|
it('should skip local auth when both useRemoteProxy and authToken are truthy', () => {
|
||||||
|
const useRemoteProxy = true;
|
||||||
|
const proxyConfig = { authToken: 'test-token-123' };
|
||||||
|
|
||||||
|
const skipLocalAuth = useRemoteProxy && proxyConfig.authToken;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT skip local auth when useRemoteProxy is false', () => {
|
||||||
|
const useRemoteProxy = false;
|
||||||
|
const proxyConfig = { authToken: 'test-token-123' };
|
||||||
|
|
||||||
|
const skipLocalAuth = useRemoteProxy && proxyConfig.authToken;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT skip local auth when authToken is undefined', () => {
|
||||||
|
const useRemoteProxy = true;
|
||||||
|
const proxyConfig = { authToken: undefined };
|
||||||
|
|
||||||
|
const skipLocalAuth = useRemoteProxy && proxyConfig.authToken;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT skip local auth when authToken is empty string', () => {
|
||||||
|
const useRemoteProxy = true;
|
||||||
|
const proxyConfig = { authToken: '' };
|
||||||
|
|
||||||
|
const skipLocalAuth = useRemoteProxy && proxyConfig.authToken;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT skip local auth when both are falsy', () => {
|
||||||
|
const useRemoteProxy = false;
|
||||||
|
const proxyConfig = { authToken: undefined };
|
||||||
|
|
||||||
|
const skipLocalAuth = useRemoteProxy && proxyConfig.authToken;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OAuth check bypass scenarios', () => {
|
||||||
|
it('should document that OAuth is skipped for remote proxy with auth', () => {
|
||||||
|
// This test documents the expected behavior:
|
||||||
|
// When using remote proxy with auth token, the remote server
|
||||||
|
// already has its own OAuth sessions, so local OAuth is unnecessary
|
||||||
|
const scenario = {
|
||||||
|
useRemoteProxy: true,
|
||||||
|
authToken: 'bearer-token',
|
||||||
|
providerRequiresOAuth: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken;
|
||||||
|
const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeTruthy();
|
||||||
|
expect(shouldTriggerLocalOAuth).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should document that OAuth runs when no remote proxy', () => {
|
||||||
|
const scenario = {
|
||||||
|
useRemoteProxy: false,
|
||||||
|
authToken: undefined,
|
||||||
|
providerRequiresOAuth: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken;
|
||||||
|
const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeFalsy();
|
||||||
|
expect(shouldTriggerLocalOAuth).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should document that OAuth runs when remote proxy has no auth token', () => {
|
||||||
|
// Edge case: remote proxy configured but no auth token
|
||||||
|
// This should fall back to local OAuth
|
||||||
|
const scenario = {
|
||||||
|
useRemoteProxy: true,
|
||||||
|
authToken: undefined,
|
||||||
|
providerRequiresOAuth: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken;
|
||||||
|
const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(skipLocalAuth).toBeFalsy();
|
||||||
|
expect(shouldTriggerLocalOAuth).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('preflight quota check bypass', () => {
|
||||||
|
it('should skip preflight for agy provider when using remote proxy with auth', () => {
|
||||||
|
const provider = 'agy';
|
||||||
|
const skipLocalAuth = true;
|
||||||
|
|
||||||
|
const shouldRunPreflight = provider === 'agy' && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(shouldRunPreflight).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run preflight for agy provider when using local mode', () => {
|
||||||
|
const provider = 'agy';
|
||||||
|
const skipLocalAuth = false;
|
||||||
|
|
||||||
|
const shouldRunPreflight = provider === 'agy' && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(shouldRunPreflight).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run preflight for non-agy providers regardless of mode', () => {
|
||||||
|
const providers = ['gemini', 'codex', 'copilot', 'kiro'];
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
const shouldRunPreflight = provider === 'agy' && !false;
|
||||||
|
expect(shouldRunPreflight).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('model configuration bypass', () => {
|
||||||
|
it('should skip model config when using remote proxy with auth', () => {
|
||||||
|
const supportsModelConfig = true;
|
||||||
|
const skipLocalAuth = true;
|
||||||
|
|
||||||
|
const shouldConfigureModel = supportsModelConfig && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(shouldConfigureModel).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run model config when using local mode', () => {
|
||||||
|
const supportsModelConfig = true;
|
||||||
|
const skipLocalAuth = false;
|
||||||
|
|
||||||
|
const shouldConfigureModel = supportsModelConfig && !skipLocalAuth;
|
||||||
|
|
||||||
|
expect(shouldConfigureModel).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('broken model warning bypass', () => {
|
||||||
|
it('should skip broken model warning when using remote proxy with auth', () => {
|
||||||
|
const skipLocalAuth = true;
|
||||||
|
const currentModel = 'some-broken-model';
|
||||||
|
const isModelBroken = true;
|
||||||
|
|
||||||
|
// Logic: only warn if NOT skipping local auth
|
||||||
|
const shouldWarn = !skipLocalAuth && currentModel && isModelBroken;
|
||||||
|
|
||||||
|
expect(shouldWarn).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show broken model warning when using local mode', () => {
|
||||||
|
const skipLocalAuth = false;
|
||||||
|
const currentModel = 'some-broken-model';
|
||||||
|
const isModelBroken = true;
|
||||||
|
|
||||||
|
const shouldWarn = !skipLocalAuth && currentModel && isModelBroken;
|
||||||
|
|
||||||
|
expect(shouldWarn).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CI/CD workflow scenarios', () => {
|
||||||
|
it('should enable headless CI workflow with remote proxy', () => {
|
||||||
|
// Simulate GitHub Actions workflow configuration
|
||||||
|
const workflowConfig = {
|
||||||
|
headless: true,
|
||||||
|
proxyHost: 'proxy.example.com',
|
||||||
|
proxyPort: 443,
|
||||||
|
proxyProtocol: 'https',
|
||||||
|
proxyAuthToken: 'github-secret-token',
|
||||||
|
remoteOnly: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if this configuration should skip local OAuth
|
||||||
|
const useRemoteProxy = !!workflowConfig.proxyHost;
|
||||||
|
const skipLocalAuth = useRemoteProxy && !!workflowConfig.proxyAuthToken;
|
||||||
|
|
||||||
|
expect(useRemoteProxy).toBe(true);
|
||||||
|
expect(skipLocalAuth).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require local OAuth when no proxy configured', () => {
|
||||||
|
// Simulate local development without proxy
|
||||||
|
const localConfig = {
|
||||||
|
headless: false,
|
||||||
|
proxyHost: undefined,
|
||||||
|
proxyAuthToken: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRemoteProxy = !!localConfig.proxyHost;
|
||||||
|
const skipLocalAuth = useRemoteProxy && !!localConfig.proxyAuthToken;
|
||||||
|
|
||||||
|
expect(useRemoteProxy).toBe(false);
|
||||||
|
expect(skipLocalAuth).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user