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:
Tam Nhu Tran
2026-02-25 15:52:40 +07:00
parent d2d1d599cd
commit 8a2a7c3eb0
4 changed files with 153 additions and 14 deletions
+144 -3
View File
@@ -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,
+4 -6
View File
@@ -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';
}
}
+3 -3
View File
@@ -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`
);
}
}
+2 -2
View File
@@ -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);
});