Files
ccs/src/cliproxy/executor/arg-parser.ts
T
Tam Nhu Tran 87aeb8f193 feat(logging): P3 hotpath console.error migration + redaction gate (928->267)
Epic P3. Migrates hotpath console.error/warn to the structured logger
(diagnostics) or process.stderr.write (user-facing), and adds a redaction
safety gate so the migration cannot leak credentials.

Redaction gate (MR1):
- log-redaction: scrub known credential token shapes (sk-ant, sk-, xoxb,
  ghp, glpat, AIza, JWT bodies, api_key=, Bearer/Basic/Token scheme) in
  string values, Error.message, AND the log message string (defense-in-depth).
- logger: message now passes through maskSecretTokens.

tool-sanitization-proxy: deleted the private file-logging subsystem
(initLogFile/writeLog/log/warn, logFilePath, debugMode); 13 call sites now
route through the existing createLogger('cliproxy:tool-sanitization-proxy').

Sweep (~120 diagnostic -> structured createLogger; ~540 user-facing -> stderr):
- diagnostics converted across proxy, web-server/routes, glmt pipeline, quota
  fetchers, executors, delegation, session-bridge, https-tunnel-proxy.
- user-facing CLI output (flows, arg-parser usage, installers, prompts, adapter
  launch errors, error display) moved to process.stderr.write (preserves stderr).
- src/utils/error-manager.ts reclassified CLI-UX-exempt (user-facing display).

Metric: hotpath console.error 928 -> 267 (71%); createLogger files 35 -> 64.
Residual 267 is user-facing CLI output (not diagnostics); documented in
docs/hardening-debt-burndown.md. Redaction gate makes further conversion safe.

Tests: hotpath-redaction-regression (12 token shapes); updated delegation-handler,
arg-parser, model-warnings spies (console.error -> process.stderr.write).
validate + validate:ci-parity green.
2026-06-18 18:48:12 -04:00

642 lines
20 KiB
TypeScript

/**
* CLIProxy Executor Arg Parser
*
* Extracted from index.ts (Concern A):
* - readOptionValue
* - hasGitLabTokenLoginFlag / getGitLabTokenLoginFlagName
* - CCS_FLAGS constant + filterCcsFlags()
* - parseExecutorFlags() — flag extraction block (lines ~411-639 in original)
* - validateFlagCombinations() — cross-flag guard block (lines ~531-585)
*
* IMPORTANT: process.exit semantics are kept identical to original index.ts,
* with explicit parseFailed/validation return state for callers that must not
* depend on ambient process.exitCode.
* All console.error messages are byte-identical.
*/
import { fail } from '../../utils/ui/indicators';
import {
isKiroAuthMethod,
isKiroIDCFlow,
type KiroAuthMethod,
type KiroIDCFlow,
normalizeKiroAuthMethod,
normalizeKiroIDCFlow,
} from '../auth/auth-types';
import type { UnifiedConfig } from '../../config/unified-config-types';
import { PROXY_CLI_FLAGS } from '../proxy/proxy-config-resolver';
import { parseThinkingOverride } from './thinking-arg-parser';
// Inlined from antigravity-responsibility.ts to avoid pulling in unified-config-loader
// (which requires js-yaml at runtime). Keep in sync if ANTIGRAVITY_ACCEPT_RISK_FLAGS changes.
const ANTIGRAVITY_ACCEPT_RISK_FLAGS_LOCAL = [
'--accept-agr-risk',
'--accept-antigravity-risk',
] as const;
function hasAntigravityRiskAcceptanceFlag(args: string[]): boolean {
return args.some((arg) =>
(ANTIGRAVITY_ACCEPT_RISK_FLAGS_LOCAL as readonly string[]).includes(arg)
);
}
// ── Simple Helpers ────────────────────────────────────────────────────────────
/**
* Parse a flag value from args supporting both `--flag value` and `--flag=value` forms.
* Returns the shape used throughout the executor:
* { present, value?, missingValue }
*/
export function readOptionValue(
args: string[],
flag: string
): { present: boolean; value?: string; missingValue: boolean } {
const inlinePrefix = `${flag}=`;
const inlineArg = args.find((arg) => arg.startsWith(inlinePrefix));
if (inlineArg !== undefined) {
const value = inlineArg.slice(inlinePrefix.length).trim();
return {
present: true,
value: value.length > 0 ? value : undefined,
missingValue: value.length === 0,
};
}
const index = args.indexOf(flag);
if (index === -1) {
return { present: false, missingValue: false };
}
const next = args[index + 1];
if (!next || next.startsWith('-')) {
return { present: true, missingValue: true };
}
return { present: true, value: next.trim(), missingValue: false };
}
/** Returns true if args contain a GitLab token-login flag. */
export function hasGitLabTokenLoginFlag(args: string[]): boolean {
return args.includes('--gitlab-token-login') || args.includes('--token-login');
}
/** Returns the specific flag name present in args (used for error messages). */
export function getGitLabTokenLoginFlagName(
args: string[]
): '--gitlab-token-login' | '--token-login' {
return args.includes('--gitlab-token-login') ? '--gitlab-token-login' : '--token-login';
}
// ── CCS Flags Filter ──────────────────────────────────────────────────────────
/**
* CCS-specific flags that must not be forwarded to the underlying Claude CLI.
* The list is kept here as the single source of truth (previously inlined in index.ts).
*/
export const CCS_FLAGS: readonly string[] = [
'--auth',
'--paste-callback',
'--port-forward',
'--headless',
'--logout',
'--config',
'--add',
'--accounts',
'--use',
'--nickname',
'--kiro-auth-method',
'--kiro-idc-start-url',
'--kiro-idc-region',
'--kiro-idc-flow',
'--thinking',
'--effort',
'--1m',
'--no-1m',
'--incognito',
'--no-incognito',
'--import',
'--accept-agr-risk',
'--accept-antigravity-risk',
'--settings',
...PROXY_CLI_FLAGS,
] as const;
/**
* Filter all CCS-specific flags (and their value arguments) from args
* before forwarding to the Claude CLI.
* Mirrors the filter logic from index.ts lines ~1328-1349.
*/
export function filterCcsFlags(args: string[]): string[] {
return args.filter((arg, idx) => {
if (CCS_FLAGS.includes(arg)) return false;
if (arg.startsWith('--kiro-auth-method=')) return false;
if (arg.startsWith('--kiro-idc-start-url=')) return false;
if (arg.startsWith('--kiro-idc-region=')) return false;
if (arg.startsWith('--kiro-idc-flow=')) return false;
if (arg.startsWith('--thinking=')) return false;
if (arg.startsWith('--effort=')) return false;
if (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false;
if (
args[idx - 1] === '--use' ||
args[idx - 1] === '--nickname' ||
args[idx - 1] === '--kiro-auth-method' ||
args[idx - 1] === '--kiro-idc-start-url' ||
args[idx - 1] === '--kiro-idc-region' ||
args[idx - 1] === '--kiro-idc-flow' ||
args[idx - 1] === '--thinking' ||
args[idx - 1] === '--effort'
)
return false;
return true;
});
}
// ── ParsedExecutorFlags ───────────────────────────────────────────────────────
/** Result of parsing CCS executor flags from args. */
export interface ParsedExecutorFlags {
parseFailed?: boolean;
forceAuth: boolean;
pasteCallback: boolean;
portForward: boolean;
forceHeadless: boolean;
forceLogout: boolean;
forceConfig: boolean;
addAccount: boolean;
showAccounts: boolean;
forceImport: boolean;
gitlabTokenLogin: boolean;
acceptAgyRisk: boolean;
incognitoFlag: boolean;
noIncognitoFlag: boolean;
noIncognito: boolean;
useAccount: string | undefined;
setNickname: string | undefined;
kiroAuthMethod: KiroAuthMethod | undefined;
kiroIDCStartUrl: string | undefined;
kiroIDCRegion: string | undefined;
kiroIDCFlow: KiroIDCFlow | undefined;
gitlabBaseUrl: string | undefined;
extendedContextOverride: boolean | undefined;
thinkingParse: ReturnType<typeof parseThinkingOverride>;
}
/**
* Parse all CCS executor flags from args.
*
* Exits with code 1 (process.exitCode = 1 + parseFailed return) on invalid flag values.
* Exits with process.exit(1) on conflicting flag combinations — identical to
* the original index.ts behavior.
*
* @param args args AFTER proxy flags have been stripped (argsWithoutProxy)
* @param context provider context needed for kiro/incognito defaults
*/
export function parseExecutorFlags(
args: string[],
context: {
provider: string;
compositeProviders: string[];
unifiedConfig: UnifiedConfig;
}
): ParsedExecutorFlags {
const { provider, unifiedConfig } = context;
const forceAuth = args.includes('--auth');
const pasteCallback = args.includes('--paste-callback');
const portForward = args.includes('--port-forward');
const forceHeadless = args.includes('--headless');
if (pasteCallback && portForward) {
process.stderr.write(String(fail('Cannot use --paste-callback with --port-forward')) + '\n');
process.stderr.write(' --paste-callback: Manually paste OAuth redirect URL\n');
process.stderr.write(' --port-forward: Use SSH port forwarding for callback\n');
process.exit(1);
}
const forceLogout = args.includes('--logout');
const forceConfig = args.includes('--config');
const addAccount = args.includes('--add');
const showAccounts = args.includes('--accounts');
const forceImport = args.includes('--import');
const gitlabTokenLogin = hasGitLabTokenLoginFlag(args);
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(args);
const incognitoFlag = args.includes('--incognito');
const noIncognitoFlag = args.includes('--no-incognito');
const kiroNoIncognitoConfig =
provider === 'kiro' ? (unifiedConfig.cliproxy?.kiro_no_incognito ?? true) : false;
const noIncognito = incognitoFlag ? false : noIncognitoFlag || kiroNoIncognitoConfig;
// Parse --use flag
let useAccount: string | undefined;
const useIdx = args.indexOf('--use');
if (useIdx !== -1 && args[useIdx + 1] && !args[useIdx + 1].startsWith('-')) {
useAccount = args[useIdx + 1];
}
// Parse --nickname flag
let setNickname: string | undefined;
const nicknameIdx = args.indexOf('--nickname');
if (nicknameIdx !== -1 && args[nicknameIdx + 1] && !args[nicknameIdx + 1].startsWith('-')) {
setNickname = args[nicknameIdx + 1];
}
// Parse --kiro-auth-method flag
let kiroAuthMethod: KiroAuthMethod | undefined;
const kiroMethodValue = readOptionValue(args, '--kiro-auth-method');
if (kiroMethodValue.present) {
const rawMethod = kiroMethodValue.value;
if (kiroMethodValue.missingValue || !rawMethod) {
process.stderr.write(String(fail('--kiro-auth-method requires a value')) + '\n');
process.stderr.write(' Supported values: aws, aws-authcode, google, github, idc\n');
process.exitCode = 1;
// Caller must check parseFailed and bail — matching original return behavior
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod: undefined,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
const normalized = rawMethod.trim().toLowerCase();
if (!isKiroAuthMethod(normalized)) {
process.stderr.write(String(fail(`Invalid --kiro-auth-method value: ${rawMethod}`)) + '\n');
process.stderr.write(' Supported values: aws, aws-authcode, google, github, idc\n');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod: undefined,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
}
let kiroIDCStartUrl: string | undefined;
const kiroIDCStartUrlValue = readOptionValue(args, '--kiro-idc-start-url');
if (kiroIDCStartUrlValue.present && kiroIDCStartUrlValue.value) {
kiroIDCStartUrl = kiroIDCStartUrlValue.value;
} else if (kiroIDCStartUrlValue.present) {
process.stderr.write(String(fail('--kiro-idc-start-url requires a value')) + '\n');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
let kiroIDCRegion: string | undefined;
const kiroIDCRegionValue = readOptionValue(args, '--kiro-idc-region');
if (kiroIDCRegionValue.present && kiroIDCRegionValue.value) {
kiroIDCRegion = kiroIDCRegionValue.value;
} else if (kiroIDCRegionValue.present) {
process.stderr.write(String(fail('--kiro-idc-region requires a value')) + '\n');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
let kiroIDCFlow: KiroIDCFlow | undefined;
const kiroIDCFlowValue = readOptionValue(args, '--kiro-idc-flow');
if (kiroIDCFlowValue.present) {
const rawFlow = kiroIDCFlowValue.value;
if (kiroIDCFlowValue.missingValue || !rawFlow) {
process.stderr.write(String(fail('--kiro-idc-flow requires a value')) + '\n');
process.stderr.write(' Supported values: authcode, device\n');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
const normalized = rawFlow.trim().toLowerCase();
if (!isKiroIDCFlow(normalized)) {
process.stderr.write(String(fail(`Invalid --kiro-idc-flow value: ${rawFlow}`)) + '\n');
process.stderr.write(' Supported values: authcode, device\n');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
}
let gitlabBaseUrl: string | undefined;
const gitlabBaseUrlValue = readOptionValue(args, '--gitlab-url');
if (gitlabBaseUrlValue.present && gitlabBaseUrlValue.value) {
gitlabBaseUrl = gitlabBaseUrlValue.value.trim();
} else if (gitlabBaseUrlValue.present) {
process.stderr.write(String(fail('--gitlab-url requires a value')) + '\n');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
parseFailed: true,
});
}
// Parse --thinking / --effort flags (aliases; first occurrence wins)
const thinkingParse = parseThinkingOverride(args);
if (thinkingParse.error) {
const { flag } = thinkingParse.error;
process.stderr.write(String(fail(`${flag} requires a value`)) + '\n');
if (provider === 'codex') {
process.stderr.write(' Codex examples: --effort xhigh, --effort high, --effort medium\n');
process.stderr.write(' Alias: --thinking xhigh (same behavior)\n');
} else {
process.stderr.write(' Examples: --thinking low, --thinking 8192, --thinking off\n');
process.stderr.write(' Levels: minimal, low, medium, high, xhigh, max, auto\n');
}
process.exit(1);
}
// Parse --1m / --no-1m flags for extended context (1M token window)
let extendedContextOverride: boolean | undefined;
const has1mFlag = args.includes('--1m') || args.some((arg) => arg.startsWith('--1m='));
const hasNo1mFlag = args.includes('--no-1m') || args.some((arg) => arg.startsWith('--no-1m='));
if (has1mFlag && hasNo1mFlag) {
process.stderr.write(String(fail('Cannot use both --1m and --no-1m flags')) + '\n');
process.exit(1);
} else if (has1mFlag) {
extendedContextOverride = true;
} else if (hasNo1mFlag) {
extendedContextOverride = false;
}
// Auto-set kiroAuthMethod = 'idc' if IDC sub-flags present without explicit method
if (!kiroAuthMethod && (kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)) {
kiroAuthMethod = 'idc';
}
return {
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabBaseUrl,
extendedContextOverride,
thinkingParse,
parseFailed: false,
};
}
/** Internal helper — builds a ParsedExecutorFlags from raw fields (avoids repeating the full struct). */
function buildPartialFlags(fields: ParsedExecutorFlags): ParsedExecutorFlags {
return fields;
}
// ── Cross-Flag Validation ─────────────────────────────────────────────────────
/**
* Validate flag combinations that are mutually exclusive or provider-scoped.
* Sets process.exitCode=1 and returns false on any violation.
* Call AFTER parseExecutorFlags() and only if parseFailed is false.
*
* @param parsed Result of parseExecutorFlags()
* @param context Provider context (provider string + compositeProviders list)
* @param args Raw argsWithoutProxy (needed for getGitLabTokenLoginFlagName)
*/
export function validateFlagCombinations(
parsed: ParsedExecutorFlags,
context: { provider: string; compositeProviders: string[] },
args: string[]
): boolean {
const { provider, compositeProviders } = context;
const {
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabTokenLogin,
gitlabBaseUrl,
} = parsed;
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
process.stderr.write(String(fail('--kiro-auth-method is only valid for ccs kiro')) + '\n');
process.exitCode = 1;
return false;
}
if (
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow) &&
provider !== 'kiro' &&
!compositeProviders.includes('kiro')
) {
process.stderr.write(
String(
fail(
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow are only valid for ccs kiro'
)
) + '\n'
);
process.exitCode = 1;
return false;
}
if (kiroAuthMethod === 'idc' && !kiroIDCStartUrl) {
process.stderr.write(String(fail('Kiro IDC login requires --kiro-idc-start-url')) + '\n');
process.stderr.write(
' Example: ccs kiro --auth --kiro-auth-method idc --kiro-idc-start-url https://d-xxx.awsapps.com/start\n'
);
process.exitCode = 1;
return false;
}
if (
kiroAuthMethod &&
kiroAuthMethod !== 'idc' &&
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)
) {
process.stderr.write(
String(
fail(
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow require --kiro-auth-method idc'
)
) + '\n'
);
process.exitCode = 1;
return false;
}
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
const flagName = gitlabTokenLogin ? getGitLabTokenLoginFlagName(args) : '--gitlab-url';
process.stderr.write(String(fail(`${flagName} is only valid for ccs gitlab`)) + '\n');
process.exitCode = 1;
return false;
}
return true;
}