basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6import { normalizeServeFastPathArgv } from './fast-path-argv.js';7import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js';8import { getHeadlessYoloSafetyWarning } from '../utils/headlessSafetyWarnings.js';9const HELP_AND_VERSION_FLAGS = new Set(['--help', '-h', '--version', '-v']);10const MCP_BUDGET_WARN_FRACTION = 0.75;11const NUMBER_OPTIONS = new Map([12 ['port', 'port'],13 ['maxSessions', 'max-sessions'],14 ['maxTotalSessions', 'max-total-sessions'],15 ['maxPendingPromptsPerSession', 'max-pending-prompts-per-session'],16 ['maxConnections', 'max-connections'],17 ['eventRingSize', 'event-ring-size'],18 ['mcp-client-budget', 'mcp-client-budget'],19 ['promptDeadlineMs', 'prompt-deadline-ms'],20 ['writerIdleTimeoutMs', 'writer-idle-timeout-ms'],21 ['channelIdleTimeoutMs', 'channel-idle-timeout-ms'],22 ['sessionReapIntervalMs', 'session-reap-interval-ms'],23 ['sessionIdleTimeoutMs', 'session-idle-timeout-ms'],24 ['permissionResponseTimeoutMs', 'permission-response-timeout-ms'],25 ['rateLimitPrompt', 'rate-limit-prompt'],26 ['rateLimitMutation', 'rate-limit-mutation'],27 ['rateLimitRead', 'rate-limit-read'],28 ['rateLimitWindowMs', 'rate-limit-window-ms'],29]);30const NUMBER_OPTION_BY_FLAG = invertOptionMap(NUMBER_OPTIONS);31const STRING_OPTION_BY_FLAG = new Map([32 ['hostname', 'hostname'],33 ['token', 'token'],34 ['workspace', 'workspace'],35 ['tls-cert', 'tlsCert'],36 ['tls-key', 'tlsKey'],37]);38const BOOLEAN_OPTION_BY_FLAG = new Map([39 ['require-auth', 'requireAuth'],40 ['enable-session-shell', 'enableSessionShell'],41 ['web', 'serveWebShell'],42 ['open', 'open'],43 ['http-bridge', 'http-bridge'],44 ['allow-private-auth-base-url', 'allowPrivateAuthBaseUrl'],45 ['experimental-lsp', 'experimentalLsp'],46 ['rate-limit', 'rateLimit'],47]);48function invertOptionMap(source) {49 const out = new Map();50 for (const [target, flag] of source) {51 out.set(flag, target);52 }53 return out;54}55function readOptionValue(argv, index, inlineValue) {56 if (inlineValue !== undefined) {57 return { value: inlineValue, nextIndex: index };58 }59 const value = argv[index + 1];60 if (value === undefined || value.startsWith('-')) {61 return null;62 }63 return { value, nextIndex: index + 1 };64}65function parseNumber(value) {66 if (value.trim() === '')67 return null;68 const parsed = Number(value);69 return Number.isNaN(parsed) ? null : parsed;70}71function parseBooleanValue(value) {72 if (value === 'true')73 return true;74 if (value === 'false')75 return false;76 return null;77}78function parsePositiveIntegerEnv(raw) {79 if (raw === undefined || raw === '')80 return undefined;81 const trimmed = raw?.trim();82 if (!trimmed || !/^\d+$/.test(trimmed))83 return Number.NaN;84 const parsed = Number(trimmed);85 return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : Number.NaN;86}87function isTruthyEnv(value) {88 return value === '1' || value === 'true';89}90function writeStderrLine(line) {91 process.stderr.write(line.endsWith('\n') ? line : `${line}\n`);92}93function setServeOption(options, key, value) {94 options[key] = value;95}96function getRateLimitValidationError(options) {97 for (const [name, value] of [98 ['--rate-limit-prompt', options.rateLimitPrompt],99 ['--rate-limit-mutation', options.rateLimitMutation],100 ['--rate-limit-read', options.rateLimitRead],101 ]) {102 if (value !== undefined &&103 (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)) {104 return `qwen serve: ${name} must be a positive integer.`;105 }106 }107 if (options.rateLimitWindowMs !== undefined &&108 (!Number.isFinite(options.rateLimitWindowMs) ||109 !Number.isInteger(options.rateLimitWindowMs) ||110 options.rateLimitWindowMs < 1000)) {111 return 'qwen serve: --rate-limit-window-ms must be an integer >= 1000.';112 }113 return null;114}115function getServeFastPathValidationError(parsed) {116 const mcpClientBudget = parsed.options.mcpClientBudget;117 if (mcpClientBudget !== undefined &&118 (!Number.isFinite(mcpClientBudget) ||119 !Number.isInteger(mcpClientBudget) ||120 mcpClientBudget <= 0)) {121 return 'qwen serve: --mcp-client-budget must be a positive integer.';122 }123 if (parsed.options.mcpBudgetMode === 'enforce' &&124 mcpClientBudget === undefined) {125 return 'qwen serve: --mcp-budget-mode=enforce requires --mcp-client-budget=N.';126 }127 const maxPendingPromptsPerSession = parsed.options.maxPendingPromptsPerSession;128 if (maxPendingPromptsPerSession !== undefined &&129 maxPendingPromptsPerSession !== Number.POSITIVE_INFINITY &&130 (!Number.isFinite(maxPendingPromptsPerSession) ||131 !Number.isInteger(maxPendingPromptsPerSession) ||132 maxPendingPromptsPerSession < 0)) {133 return 'qwen serve: --max-pending-prompts-per-session must be a non-negative integer (0 / Infinity = unlimited).';134 }135 return null;136}137function blockForever() {138 return new Promise(() => { });139}140export async function waitForServeRuntimeOrExit(handle) {141 try {142 await handle.runtimeReady;143 }144 catch (err) {145 if (err instanceof Error &&146 err.message === RUNTIME_STARTUP_CANCELLED_MESSAGE) {147 return;148 }149 writeStderrLine(`qwen serve: runtime startup failed after listener was ready: ${err instanceof Error ? err.message : String(err)}`);150 await handle.close().catch(() => undefined);151 process.exit(1);152 }153}154function applyRateLimitEnvDefaults(options, env) {155 if (options.rateLimit === undefined &&156 isTruthyEnv(env['QWEN_SERVE_RATE_LIMIT'])) {157 options.rateLimit = true;158 }159 if (options.rateLimit) {160 options.rateLimitPrompt ??= parsePositiveIntegerEnv(env['QWEN_SERVE_RATE_LIMIT_PROMPT']);161 options.rateLimitMutation ??= parsePositiveIntegerEnv(env['QWEN_SERVE_RATE_LIMIT_MUTATION']);162 options.rateLimitRead ??= parsePositiveIntegerEnv(env['QWEN_SERVE_RATE_LIMIT_READ']);163 options.rateLimitWindowMs ??= parsePositiveIntegerEnv(env['QWEN_SERVE_RATE_LIMIT_WINDOW_MS']);164 }165}166function discardRateLimitTuningWhenDisabled(options) {167 if (options.rateLimit === true)168 return;169 delete options.rateLimitPrompt;170 delete options.rateLimitMutation;171 delete options.rateLimitRead;172 delete options.rateLimitWindowMs;173}174export async function bootstrapServeFastPathEnvironment(workspace) {175 const { loadServeFastPathEnvironment, loadServeFastPathSettings, preResolveServeFastPathHomeEnvOverrides, } = await import('./fast-path-settings.js');176 preResolveServeFastPathHomeEnvOverrides();177 const workspaceDir = workspace ?? process.cwd();178 const settings = loadServeFastPathSettings(workspaceDir);179 loadServeFastPathEnvironment(settings, workspaceDir);180 return settings;181}182export function parseServeFastPathArgs(rawArgv, env = process.env) {183 const argv = normalizeServeFastPathArgv(rawArgv);184 if (argv[0] !== 'serve')185 return { kind: 'fallback' };186 if (argv.some((arg) => HELP_AND_VERSION_FLAGS.has(arg))) {187 return { kind: 'fallback' };188 }189 // Keep this lightweight mirror in sync with commands/serve.ts; unsupported190 // flags intentionally fall back to the full yargs parser.191 const options = {192 hostname: '127.0.0.1',193 mode: 'http-bridge',194 port: 4170,195 };196 let open = false;197 let httpBridge = true;198 let mcpBudgetModeRaw;199 let mcpClientBudget;200 let explicitRateLimit;201 for (let i = 1; i < argv.length; i++) {202 const arg = argv[i];203 if (arg === '--')204 return { kind: 'fallback' };205 if (!arg.startsWith('--'))206 return { kind: 'fallback' };207 const withoutPrefix = arg.slice(2);208 const equalsIndex = withoutPrefix.indexOf('=');209 const rawFlag = equalsIndex === -1 ? withoutPrefix : withoutPrefix.slice(0, equalsIndex);210 const inlineValue = equalsIndex === -1 ? undefined : withoutPrefix.slice(equalsIndex + 1);211 const negated = rawFlag.startsWith('no-');212 const flag = negated ? rawFlag.slice(3) : rawFlag;213 const booleanTarget = BOOLEAN_OPTION_BY_FLAG.get(flag);214 if (booleanTarget) {215 let value = !negated;216 if (inlineValue !== undefined) {217 const parsed = parseBooleanValue(inlineValue);218 if (parsed === null || negated)219 return { kind: 'fallback' };220 value = parsed;221 }222 if (booleanTarget === 'open') {223 open = value;224 }225 else if (booleanTarget === 'http-bridge') {226 httpBridge = value;227 }228 else {229 setServeOption(options, booleanTarget, value);230 if (booleanTarget === 'rateLimit') {231 explicitRateLimit = value;232 }233 }234 continue;235 }236 if (negated)237 return { kind: 'fallback' };238 const numberTarget = NUMBER_OPTION_BY_FLAG.get(flag);239 if (numberTarget) {240 const read = readOptionValue(argv, i, inlineValue);241 if (!read)242 return { kind: 'fallback' };243 i = read.nextIndex;244 const value = parseNumber(read.value);245 if (value === null)246 return { kind: 'fallback' };247 if (numberTarget === 'mcp-client-budget') {248 mcpClientBudget = value;249 }250 else {251 setServeOption(options, numberTarget, value);252 }253 continue;254 }255 const stringTarget = STRING_OPTION_BY_FLAG.get(flag);256 if (stringTarget) {257 const read = readOptionValue(argv, i, inlineValue);258 if (!read)259 return { kind: 'fallback' };260 i = read.nextIndex;261 if (stringTarget === 'workspace' &&262 (options.workspace !== undefined || read.value === '')) {263 return { kind: 'fallback' };264 }265 setServeOption(options, stringTarget, read.value);266 continue;267 }268 if (flag === 'mcp-budget-mode') {269 const read = readOptionValue(argv, i, inlineValue);270 if (!read)271 return { kind: 'fallback' };272 i = read.nextIndex;273 mcpBudgetModeRaw = read.value;274 continue;275 }276 if (flag === 'allow-origin') {277 const read = readOptionValue(argv, i, inlineValue);278 if (!read)279 return { kind: 'fallback' };280 i = read.nextIndex;281 options.allowOrigins = [...(options.allowOrigins ?? []), read.value];282 continue;283 }284 return { kind: 'fallback' };285 }286 if (mcpBudgetModeRaw !== undefined &&287 mcpBudgetModeRaw !== 'enforce' &&288 mcpBudgetModeRaw !== 'warn' &&289 mcpBudgetModeRaw !== 'off') {290 return { kind: 'fallback' };291 }292 const mcpBudgetMode = mcpBudgetModeRaw ??293 (mcpClientBudget !== undefined ? 'warn' : 'off');294 if (mcpClientBudget !== undefined)295 options.mcpClientBudget = mcpClientBudget;296 options.mcpBudgetMode = mcpBudgetMode;297 if (explicitRateLimit !== undefined) {298 options.rateLimit = explicitRateLimit;299 }300 applyRateLimitEnvDefaults(options, env);301 return { kind: 'serve', open, httpBridge, options };302}303async function maybeOpenWebShellBrowser(handle, open) {304 if (!open)305 return;306 try {307 await handle.runtimeReady;308 }309 catch {310 return;311 }312 const { maybeOpenWebShellBrowser: openBrowser } = await import('../commands/serve.js');313 await openBrowser(handle, true);314}315function emitHeadlessYoloWarning(settings) {316 if (!settings)317 return;318 const warning = getHeadlessYoloSafetyWarning({319 getApprovalMode: () => settings.tools?.approvalMode,320 getSandbox: () => settings.tools?.sandbox,321 });322 if (warning) {323 writeStderrLine(warning);324 }325}326function writeServeWarnings(parsed) {327 if (!parsed.httpBridge) {328 writeStderrLine('qwen serve: --no-http-bridge (native mode) is not yet implemented; ' +329 'falling back to http-bridge.');330 }331 if (parsed.options.token) {332 writeStderrLine('qwen serve: --token is visible in the process command line; ' +333 'prefer the QWEN_SERVER_TOKEN env var for any non-trivial deployment.');334 }335 const mcpClientBudget = parsed.options.mcpClientBudget;336 if (mcpClientBudget !== undefined) {337 const resolvedMcpMode = parsed.options.mcpBudgetMode ?? 'warn';338 writeStderrLine(`qwen serve: --mcp-client-budget=${mcpClientBudget} mode=${resolvedMcpMode}` +339 (resolvedMcpMode === 'enforce'340 ? ' (servers past the cap will be refused at discovery)'341 : resolvedMcpMode === 'warn'342 ? ` (warnings at >=${Math.ceil(mcpClientBudget * MCP_BUDGET_WARN_FRACTION)}, no refusal)`343 : ''));344 }345}346export async function tryRunServeFastPath(rawArgv = process.argv.slice(2)) {347 const parsed = parseServeFastPathArgs(rawArgv);348 if (parsed.kind === 'fallback')349 return false;350 const validationError = getServeFastPathValidationError(parsed) ||351 (parsed.options.rateLimit === true352 ? getRateLimitValidationError(parsed.options)353 : null);354 if (validationError) {355 writeStderrLine(validationError);356 process.exit(1);357 }358 let settings;359 try {360 settings = await bootstrapServeFastPathEnvironment(parsed.options.workspace);361 }362 catch (err) {363 writeStderrLine(`qwen serve: fast-path bootstrap failed, falling back to full startup: ${err instanceof Error ? err.message : String(err)}`);364 return false;365 }366 applyRateLimitEnvDefaults(parsed.options, process.env);367 discardRateLimitTuningWhenDisabled(parsed.options);368 const rateLimitError = getRateLimitValidationError(parsed.options);369 if (rateLimitError) {370 writeStderrLine(rateLimitError);371 process.exit(1);372 }373 writeServeWarnings(parsed);374 const { runQwenServe } = await import('./run-qwen-serve.js');375 let handle;376 try {377 handle = await runQwenServe(parsed.options, {378 ...(settings ? { bootSettings: settings } : {}),379 resolveOnListen: true,380 deferRuntimeUntilFirstHealth: !parsed.open,381 });382 try {383 emitHeadlessYoloWarning(settings);384 }385 catch {386 // Keep the warning best-effort, matching the yargs serve handler.387 }388 await maybeOpenWebShellBrowser(handle, parsed.open);389 }390 catch (err) {391 writeStderrLine(`qwen serve: ${err instanceof Error ? err.message : String(err)}`);392 process.exit(1);393 }394 await waitForServeRuntimeOrExit(handle);395 await blockForever();396 return true;397}398//# sourceMappingURL=fast-path.js.map