mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
feat(runtime): run cliproxy profiles on droid
- execute cliproxy profiles on droid using resolved proxy env credentials - enforce auth/service preconditions and block Claude-only management flags - allow dotted profile names in droid adapter/config manager
This commit is contained in:
+144
-3
@@ -12,7 +12,14 @@ import {
|
||||
import { expandPath } from './utils/helpers';
|
||||
import { validateGlmKey, validateMiniMaxKey } from './utils/api-key-validator';
|
||||
import { ErrorManager } from './utils/error-manager';
|
||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
||||
import {
|
||||
execClaudeWithCLIProxy,
|
||||
CLIProxyProvider,
|
||||
ensureCliproxyService,
|
||||
isAuthenticated,
|
||||
} from './cliproxy';
|
||||
import { getEffectiveEnvVars, getCompositeEnvVars } from './cliproxy/config/env-builder';
|
||||
import { CLIPROXY_DEFAULT_PORT } from './cliproxy/config/port-manager';
|
||||
import {
|
||||
ensureMcpWebSearch,
|
||||
displayWebSearchStatus,
|
||||
@@ -694,7 +701,9 @@ async function main(): Promise<void> {
|
||||
if (resolvedTarget === 'droid') {
|
||||
try {
|
||||
const allProfiles = detector.getAllProfiles();
|
||||
const activeProfiles = allProfiles.settings.filter((name) => /^[a-zA-Z0-9_-]+$/.test(name));
|
||||
const activeProfiles = allProfiles.settings.filter((name) =>
|
||||
/^[a-zA-Z0-9._-]+$/.test(name)
|
||||
);
|
||||
await pruneOrphanedModels(activeProfiles);
|
||||
} catch (error) {
|
||||
console.error(warn(`[!] Droid prune skipped: ${(error as Error).message}`));
|
||||
@@ -724,9 +733,141 @@ async function main(): Promise<void> {
|
||||
const provider = profileInfo.provider || (profileInfo.name as CLIProxyProvider);
|
||||
const customSettingsPath = profileInfo.settingsPath; // undefined for hardcoded profiles
|
||||
const variantPort = profileInfo.port; // variant-specific port for isolation
|
||||
const cliproxyPort = variantPort || CLIPROXY_DEFAULT_PORT;
|
||||
|
||||
if (resolvedTarget !== 'claude') {
|
||||
const adapter = targetAdapter;
|
||||
if (!adapter) {
|
||||
console.error(fail(`Target adapter not found for "${resolvedTarget}"`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (!adapter.supportsProfileType('cliproxy')) {
|
||||
console.error(fail(`${adapter.displayName} does not support CLIProxy profiles`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep CLIProxy management/auth flags on Claude flow only.
|
||||
const unsupportedCliproxyFlags = [
|
||||
'--auth',
|
||||
'--logout',
|
||||
'--accounts',
|
||||
'--add',
|
||||
'--use',
|
||||
'--config',
|
||||
'--headless',
|
||||
'--paste-callback',
|
||||
'--port-forward',
|
||||
'--nickname',
|
||||
'--kiro-auth-method',
|
||||
'--backend',
|
||||
'--proxy-host',
|
||||
'--proxy-port',
|
||||
'--proxy-protocol',
|
||||
'--proxy-auth-token',
|
||||
'--proxy-timeout',
|
||||
'--local-proxy',
|
||||
'--remote-only',
|
||||
'--no-fallback',
|
||||
'--allow-self-signed',
|
||||
'--thinking',
|
||||
'--effort',
|
||||
'--1m',
|
||||
'--no-1m',
|
||||
];
|
||||
const providedUnsupportedFlag = unsupportedCliproxyFlags.find(
|
||||
(flag) =>
|
||||
remainingArgs.includes(flag) || remainingArgs.some((arg) => arg.startsWith(`${flag}=`))
|
||||
);
|
||||
if (providedUnsupportedFlag) {
|
||||
console.error(
|
||||
fail(
|
||||
`${providedUnsupportedFlag} is only supported when running CLIProxy profiles on Claude target`
|
||||
)
|
||||
);
|
||||
console.error(
|
||||
info(`Run with Claude target: ccs ${profileInfo.name} --target claude ...`)
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// For Droid execution path, require existing OAuth auth and running local proxy.
|
||||
if (profileInfo.isComposite && profileInfo.compositeTiers) {
|
||||
const compositeProviders = [
|
||||
...new Set(Object.values(profileInfo.compositeTiers).map((tier) => tier.provider)),
|
||||
] as CLIProxyProvider[];
|
||||
const missingProvider = compositeProviders.find((p) => !isAuthenticated(p));
|
||||
if (missingProvider) {
|
||||
console.error(
|
||||
fail(`Missing OAuth auth for composite tier provider: ${missingProvider}`)
|
||||
);
|
||||
console.error(info(`Authenticate first: ccs ${missingProvider} --auth`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
} else if (!isAuthenticated(provider)) {
|
||||
console.error(fail(`No OAuth authentication found for provider: ${provider}`));
|
||||
console.error(info(`Authenticate first: ccs ${provider} --auth`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const ensureServiceResult = await ensureCliproxyService(
|
||||
cliproxyPort,
|
||||
remainingArgs.includes('--verbose') || remainingArgs.includes('-v')
|
||||
);
|
||||
if (!ensureServiceResult.started) {
|
||||
console.error(
|
||||
fail(ensureServiceResult.error || 'Failed to start local CLIProxy service')
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const envVars =
|
||||
profileInfo.isComposite && profileInfo.compositeTiers && profileInfo.compositeDefaultTier
|
||||
? getCompositeEnvVars(
|
||||
profileInfo.compositeTiers,
|
||||
profileInfo.compositeDefaultTier,
|
||||
cliproxyPort,
|
||||
customSettingsPath
|
||||
)
|
||||
: getEffectiveEnvVars(provider, cliproxyPort, customSettingsPath);
|
||||
|
||||
const creds: TargetCredentials = {
|
||||
profile: profileInfo.name,
|
||||
baseUrl: envVars['ANTHROPIC_BASE_URL'] || '',
|
||||
apiKey: envVars['ANTHROPIC_AUTH_TOKEN'] || '',
|
||||
model: envVars['ANTHROPIC_MODEL'] || undefined,
|
||||
provider: 'anthropic',
|
||||
envVars,
|
||||
};
|
||||
|
||||
if (!creds.baseUrl || !creds.apiKey) {
|
||||
console.error(
|
||||
fail(
|
||||
`Missing CLIProxy runtime credentials for ${profileInfo.name} (ANTHROPIC_BASE_URL/AUTH_TOKEN)`
|
||||
)
|
||||
);
|
||||
console.error(
|
||||
info('Reconfigure with: ccs config > CLIProxy, or run ccs <provider> --config')
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await adapter.prepareCredentials(creds);
|
||||
const targetArgs = adapter.buildArgs(profileInfo.name, remainingArgs);
|
||||
const targetEnv = adapter.buildEnv(creds, profileInfo.type);
|
||||
adapter.exec(targetArgs, targetEnv, { binaryInfo: targetBinaryInfo || undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
await execClaudeWithCLIProxy(claudeCli, provider, remainingArgs, {
|
||||
customSettingsPath,
|
||||
port: variantPort,
|
||||
port: cliproxyPort,
|
||||
isComposite: profileInfo.isComposite,
|
||||
compositeTiers: profileInfo.compositeTiers,
|
||||
compositeDefaultTier: profileInfo.compositeDefaultTier,
|
||||
|
||||
@@ -53,9 +53,9 @@ export class DroidAdapter implements TargetAdapter {
|
||||
}
|
||||
|
||||
buildArgs(profile: string, userArgs: string[]): string[] {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(profile)) {
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(profile)) {
|
||||
throw new Error(
|
||||
`Invalid profile name "${profile}" for Droid target: only alphanumeric, underscore, hyphen allowed`
|
||||
`Invalid profile name "${profile}" for Droid target: only alphanumeric, dot, underscore, hyphen allowed`
|
||||
);
|
||||
}
|
||||
return ['-m', `custom:ccs-${profile}`, ...userArgs];
|
||||
@@ -154,10 +154,8 @@ export class DroidAdapter implements TargetAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Droid currently supports direct settings-based and default flows only.
|
||||
*/
|
||||
/** Droid supports settings/default and CLIProxy-executed profile flows. */
|
||||
supportsProfileType(profileType: ProfileType): boolean {
|
||||
return profileType === 'settings' || profileType === 'default';
|
||||
return profileType === 'settings' || profileType === 'default' || profileType === 'cliproxy';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,16 @@ const LOCK_RETRY_MAX_MS = 1000;
|
||||
|
||||
/**
|
||||
* Validate profile name to prevent filesystem/security issues.
|
||||
* Only alphanumeric, underscore, hyphen allowed.
|
||||
* Only alphanumeric, dot, underscore, hyphen allowed.
|
||||
*/
|
||||
function isValidProfileName(profile: string): boolean {
|
||||
return !!profile && /^[a-zA-Z0-9_-]+$/.test(profile);
|
||||
return !!profile && /^[a-zA-Z0-9._-]+$/.test(profile);
|
||||
}
|
||||
|
||||
function validateProfileName(profile: string): void {
|
||||
if (!isValidProfileName(profile)) {
|
||||
throw new Error(
|
||||
`Invalid profile name "${profile}": must contain only alphanumeric characters, underscores, or hyphens`
|
||||
`Invalid profile name "${profile}": must contain only alphanumeric characters, dots, underscores, or hyphens`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +120,8 @@ describe('DroidAdapter', () => {
|
||||
expect(adapter.supportsProfileType('default')).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT support cliproxy and copilot profile types', () => {
|
||||
expect(adapter.supportsProfileType('cliproxy')).toBe(false);
|
||||
it('should support cliproxy and NOT support copilot profile type', () => {
|
||||
expect(adapter.supportsProfileType('cliproxy')).toBe(true);
|
||||
expect(adapter.supportsProfileType('copilot')).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user