Files
ccs/src/cliproxy/executor/proxy-resolver.ts
T

213 lines
7.2 KiB
TypeScript

/**
* Proxy Resolver — Concern D
*
* Handles proxy configuration resolution, remote proxy reachability check,
* local-backend selection, and CLIProxy binary acquisition.
*
* Extracted from executor/index.ts to isolate the proxy-resolution concern.
* All log messages, error messages, and exit semantics are byte-identical to
* the original implementation.
*/
import { ProgressIndicator } from '../../utils/progress-indicator';
import { ok, fail, info, warn } from '../../utils/ui';
import {
ensureCLIProxyBinary,
getConfiguredBackend,
getPlusBackendUnavailableMessage,
} from '../binary-manager';
import { checkRemoteProxy } from '../services/remote-proxy-client';
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types';
import { resolveProxyConfig } from '../proxy/proxy-config-resolver';
import { CLIPROXY_DEFAULT_PORT, validatePort } from '../config/config-generator';
import type { ResolvedProxyConfig } from '../types';
import type { UnifiedConfig } from '../../config/schemas/unified-config';
import { isNetworkError, handleNetworkError } from './retry-handler';
export interface ResolvedExecutorProxyConfig {
/** Resolved proxy config after merging CLI > ENV > config.yaml > defaults */
proxyConfig: ResolvedProxyConfig;
/** Args after proxy-related flags are stripped out */
argsWithoutProxy: string[];
/** Mutated executor config (port resolved and validated) */
cfg: ExecutorConfig;
}
/** Result returned from resolveExecutorProxy */
export interface ResolvedProxy extends ResolvedExecutorProxyConfig {
/** Whether to use the remote proxy (vs spawning a local one) */
useRemoteProxy: boolean;
/** Which local backend binary to use ('original' | 'plus') */
localBackend: CLIProxyBackend;
/** Absolute path to CLIProxy binary; undefined when useRemoteProxy=true */
binaryPath: string | undefined;
}
/** Dependencies injected by the orchestrator */
export interface ResolveExecutorProxyContext {
unifiedConfig: UnifiedConfig;
allProviders: CLIProxyProvider[];
verbose: boolean;
cfg: ExecutorConfig;
log: (msg: string) => void;
}
/**
* Resolves side-effect-free proxy configuration and strips proxy flags.
*
* Mutates `context.cfg.port` in-place (same as original orchestrator behaviour).
*/
export function resolveExecutorProxyConfig(
args: string[],
context: ResolveExecutorProxyContext
): ResolvedExecutorProxyConfig {
const { unifiedConfig, cfg, log } = context;
// Resolve proxy config from CLI flags > ENV > config.yaml > defaults
const cliproxyServerConfig = unifiedConfig.cliproxy_server;
const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, {
remote: cliproxyServerConfig?.remote
? {
enabled: cliproxyServerConfig.remote.enabled,
host: cliproxyServerConfig.remote.host,
port: cliproxyServerConfig.remote.port,
protocol: cliproxyServerConfig.remote.protocol,
auth_token: cliproxyServerConfig.remote.auth_token,
management_key: cliproxyServerConfig.remote.management_key,
timeout: cliproxyServerConfig.remote.timeout,
}
: undefined,
local: cliproxyServerConfig?.local
? {
port: cliproxyServerConfig.local.port,
auto_start: cliproxyServerConfig.local.auto_start,
}
: undefined,
});
// Port resolution and validation (mutates cfg in-place)
if (cfg.port && cfg.port !== CLIPROXY_DEFAULT_PORT) {
if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) {
cfg.port = proxyConfig.port;
}
} else if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) {
cfg.port = proxyConfig.port;
}
cfg.port = validatePort(cfg.port);
log(`Proxy mode: ${proxyConfig.mode}`);
if (proxyConfig.mode === 'remote') {
log(`Remote host: ${proxyConfig.host}:${proxyConfig.port} (${proxyConfig.protocol})`);
}
return { proxyConfig, argsWithoutProxy, cfg };
}
/**
* Resolves proxy configuration, checks remote reachability, selects the local
* backend, and ensures the CLIProxy binary is present when running locally.
*/
export async function resolveExecutorProxy(
resolvedConfig: ResolvedExecutorProxyConfig,
context: ResolveExecutorProxyContext
): Promise<ResolvedProxy> {
const { allProviders, verbose: _verbose } = context;
const { proxyConfig, argsWithoutProxy, cfg } = resolvedConfig;
// Check remote proxy reachability
let useRemoteProxy = false;
let localBackend: CLIProxyBackend = 'original';
if (proxyConfig.mode === 'remote' && proxyConfig.host) {
const status = await checkRemoteProxy({
host: proxyConfig.host,
port: proxyConfig.port,
protocol: proxyConfig.protocol,
authToken: proxyConfig.authToken,
timeout: proxyConfig.timeout ?? 2000,
allowSelfSigned: proxyConfig.allowSelfSigned ?? false,
});
if (status.reachable) {
useRemoteProxy = true;
console.log(
ok(
`Connected to remote proxy at ${proxyConfig.host}:${proxyConfig.port} (${status.latencyMs}ms)`
)
);
} else {
console.error(warn(`Remote proxy unreachable: ${status.error}`));
if (proxyConfig.remoteOnly) {
throw new Error('Remote proxy unreachable and --remote-only specified');
}
if (proxyConfig.fallbackEnabled) {
if (proxyConfig.autoStartLocal) {
console.log(info('Falling back to local proxy...'));
} else {
if (process.stdin.isTTY) {
const readline = await import('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise<string>((resolve) => {
rl.question('Start local proxy instead? [Y/n] ', resolve);
});
rl.close();
if (answer.toLowerCase() === 'n') {
throw new Error('Remote proxy unreachable and user declined fallback');
}
}
console.log(info('Starting local proxy...'));
}
} else {
throw new Error('Remote proxy unreachable and fallback disabled');
}
}
}
// Local backend selection (only when not using remote proxy)
if (!useRemoteProxy) {
localBackend = getConfiguredBackend({ notifyOnPlus: true });
for (const p of allProviders) {
if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) {
console.error('');
console.error(fail(getPlusBackendUnavailableMessage(p)));
console.error('');
throw new Error(`Provider ${p} requires local CLIProxy Plus backend`);
}
}
}
// Binary acquisition — skipped when using remote proxy
let binaryPath: string | undefined;
if (!useRemoteProxy) {
const spinner = new ProgressIndicator('Preparing CLIProxy');
spinner.start();
try {
binaryPath = await ensureCLIProxyBinary(_verbose, { skipAutoUpdate: true });
spinner.succeed('CLIProxy binary ready');
} catch (error) {
spinner.fail('Failed to prepare CLIProxy');
const err = error as Error;
if (isNetworkError(err)) {
handleNetworkError(err);
}
throw error;
}
}
return {
proxyConfig,
useRemoteProxy,
localBackend,
binaryPath,
argsWithoutProxy,
cfg,
};
}