/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import process from 'node:process'; import { Config, normalizeShellReplacement, type ApprovalMode, type OutputFormat, type SandboxConfig, type PolicyEngineConfig, type MCPServerConfig, } from '@vybestack/llxprt-code-core'; import { createAgentRuntimeFactoryBindings, registerActivateSkillTool, } from '@vybestack/llxprt-code-agents'; import { registerAgentRuntimeFactories } from '@vybestack/llxprt-code-providers/runtime.js'; import { getEnableHooks, getEnableHooksUI } from './settingsSchema.js'; import { loadSettings } from './settings.js'; import { appEvents } from '../utils/events.js'; import type { Settings } from './settings.js'; import type { SettingsService } from '@vybestack/llxprt-code-settings'; import type { CliArgs } from './cliArgParser.js'; import type { ContextResolutionResult } from './interactiveContext.js'; import type { ProviderModelResult } from './providerModelResolver.js'; import { firstNonEmptyString } from '../utils/coalesce.js'; import { createGitHubBrokerClient } from './githubBrokerClient.js'; // @plan PLAN-20260610-ISSUE1592.P01 // @requirement REQ-INV-001, REQ-INV-002, REQ-INV-003 // Register the concrete agent runtime factories with the providers package's // dependency-inversion seam. The implementations live in the agents package, // which depends on providers; importing them directly inside providers would // create a providers→agents cycle, so the CLI (composition root) injects them // via the curated public factory helper (#2204). const agentRuntimeFactoryBindings = createAgentRuntimeFactoryBindings(); registerAgentRuntimeFactories(agentRuntimeFactoryBindings); // ─── DTOs ─────────────────────────────────────────────────────────────────── export interface ConfigBuildInput { readonly sessionId: string; readonly cwd: string; /** * The bootstrap runtime's SettingsService. Passed explicitly because Config * construction never adopts ambient runtime state (issue #2300) — the CLI * composition boundary owns which settings instance the session uses. */ readonly settingsService: SettingsService; readonly argv: CliArgs; readonly profileSettingsWithTools: Settings; readonly context: ContextResolutionResult; readonly approvalMode: ApprovalMode; readonly providerModel: ProviderModelResult; readonly sandboxConfig: SandboxConfig | undefined; readonly mcpServers: Record; readonly blockedMcpServers: ReadonlyArray<{ name: string; extensionName: string; }>; readonly reloadMcpServers?: () => Promise<{ mcpServers: Record; blockedMcpServers: Array<{ name: string; extensionName: string }>; settingsMcpServers: Record; }>; readonly excludeTools: readonly string[]; readonly memoryContent: string; readonly fileCount: number; readonly filePaths: readonly string[]; readonly policyEngineConfig: PolicyEngineConfig; readonly question: string; readonly screenReader: boolean; readonly useRipgrepSetting: boolean | undefined; readonly mcpEnabled: boolean; readonly extensionsEnabled: boolean; readonly adminSkillsEnabled: boolean; readonly outputFormat: OutputFormat; readonly quiet: boolean; readonly allowedTools: readonly string[]; } // ─── Sub-builders ──────────────────────────────────────────────────────────── function buildTelemetryConfig(argv: CliArgs, settings: Settings) { const telemetrySettings = settings.telemetry; return { enabled: argv.telemetry ?? telemetrySettings?.enabled, logPrompts: argv.telemetryLogPrompts ?? telemetrySettings?.logPrompts, outfile: argv.telemetryOutfile ?? telemetrySettings?.outfile, logApiBodies: telemetrySettings?.logApiBodies, logApiBodyMaxChars: telemetrySettings?.logApiBodyMaxChars, outfileMaxBytes: telemetrySettings?.outfileMaxBytes, outfileMaxFiles: telemetrySettings?.outfileMaxFiles, perf: telemetrySettings?.perf, ...buildTelemetryRedactionConfig(telemetrySettings), }; } function buildTelemetryRedactionConfig( telemetrySettings: Settings['telemetry'], ) { return { logConversations: telemetrySettings?.logConversations, logResponses: telemetrySettings?.logResponses, redactSensitiveData: telemetrySettings?.redactSensitiveData, redactFilePaths: telemetrySettings?.redactFilePaths, redactUrls: telemetrySettings?.redactUrls, redactEmails: telemetrySettings?.redactEmails, redactPersonalInfo: telemetrySettings?.redactPersonalInfo, }; } function buildSanitizationConfig(settings: Settings) { return { allowedEnvironmentVariables: [ ...(settings.security?.environmentVariableRedaction?.allowed ?? []), ], blockedEnvironmentVariables: [ ...(settings.security?.environmentVariableRedaction?.blocked ?? []), ], enableEnvironmentVariableRedaction: settings.security?.environmentVariableRedaction?.enabled ?? false, }; } function buildHooksConfig( settings: Settings, adminSkillsEnabled: boolean, cwd: string, ) { const hooksConfig = settings.hooks ?? {}; const { disabled: _disabled, ...eventHooks } = hooksConfig as { disabled?: string[]; [key: string]: unknown; }; return { enableHooks: getEnableHooks(settings), enableHooksUI: getEnableHooksUI(settings), hooks: eventHooks, onReload: async () => { const refreshedSettings = loadSettings(cwd); return { disabledSkills: refreshedSettings.merged.skills?.disabled, adminSkillsEnabled: refreshedSettings.merged.admin?.skills?.enabled ?? adminSkillsEnabled, }; }, }; } function buildToolConfig( argv: CliArgs, profileSettingsWithTools: Settings, mcpEnabled: boolean, mcpServers: Record, excludeTools: readonly string[], allowedTools: readonly string[], policyEngineConfig: PolicyEngineConfig, ) { return { coreTools: profileSettingsWithTools.coreTools ?? undefined, allowedTools: allowedTools.length > 0 ? [...allowedTools] : undefined, policyEngineConfig, excludeTools: [...excludeTools], toolDiscoveryCommand: profileSettingsWithTools.toolDiscoveryCommand, toolCallCommand: profileSettingsWithTools.toolCallCommand, mcpServerCommand: mcpEnabled ? profileSettingsWithTools.mcpServerCommand : undefined, mcpServers: mcpEnabled ? mcpServers : {}, mcpEnabled, allowedMcpServers: mcpEnabled ? (argv.allowedMcpServerNames ?? profileSettingsWithTools.mcp?.allowed) : undefined, }; } function buildSessionBaseArgs( input: ConfigBuildInput, toolConfig: ReturnType, telemetry: ReturnType, sanitizationConfig: ReturnType, ) { const { sessionId, cwd, settingsService, argv, profileSettingsWithTools, context, approvalMode, providerModel, sandboxConfig, memoryContent, fileCount, filePaths, screenReader, outputFormat, quiet, question, extensionsEnabled, adminSkillsEnabled, } = input; return { sessionId, settingsService, embeddingModel: undefined, sandbox: sandboxConfig, targetDir: cwd, includeDirectories: context.includeDirectories as string[], loadMemoryFromIncludeDirectories: context.resolvedLoadMemoryFromIncludeDirectories, debugMode: context.debugMode, outputFormat, quiet, question, ...toolConfig, extensionsEnabled, adminSkillsEnabled, userMemory: memoryContent, llxprtMdFileCount: fileCount, llxprtMdFilePaths: [...filePaths], approvalMode, showMemoryUsage: argv.showMemoryUsage ?? profileSettingsWithTools.ui?.showMemoryUsage ?? false, disableYoloMode: profileSettingsWithTools.security?.disableYoloMode ?? profileSettingsWithTools.admin?.secureModeEnabled, accessibility: { ...profileSettingsWithTools.accessibility, screenReader }, telemetry, usageStatisticsEnabled: profileSettingsWithTools.ui?.usageStatisticsEnabled ?? true, fileFiltering: context.fileFiltering, checkpointing: argv.checkpointing ?? profileSettingsWithTools.checkpointing?.enabled, dumpOnError: argv.dumponerror ?? false, proxy: resolveProxy(argv.proxy), cwd, fileDiscoveryService: context.fileService, bugCommand: profileSettingsWithTools.bugCommand, model: providerModel.model, provider: providerModel.provider, sanitizationConfig, }; } function buildFeatureArgs( input: ConfigBuildInput, hooksConfig: ReturnType, ) { const { argv, profileSettingsWithTools, context, useRipgrepSetting, blockedMcpServers, } = input; return { extensionContextFilePaths: [...context.extensionContextFilePaths], maxSessionTurns: profileSettingsWithTools.ui?.maxSessionTurns ?? -1, experimentalZedIntegration: argv.experimentalAcp ?? false, listExtensions: argv.listExtensions ?? false, activeExtensions: context.activeExtensions.map((e) => ({ name: e.name, version: e.version, })), extensions: context.allExtensions, enableExtensionReloading: profileSettingsWithTools.experimental?.extensionReloading, blockedMcpServers: [...blockedMcpServers], skillsSupport: profileSettingsWithTools.experimental?.skills === true || (profileSettingsWithTools.skills?.enabled ?? true), disabledSkills: profileSettingsWithTools.skills?.disabled, noBrowser: !!process.env.NO_BROWSER, summarizeToolOutput: profileSettingsWithTools.summarizeToolOutput, ideMode: context.ideMode, chatCompression: profileSettingsWithTools.chatCompression, interactive: context.interactive, folderTrust: context.folderTrust, trustedFolder: context.trustedFolder, shellReplacement: normalizeShellReplacement( profileSettingsWithTools.shellReplacement as | 'allowlist' | 'all' | 'none' | boolean | undefined, ), useRipgrep: useRipgrepSetting, // @plan PLAN-20260731-GHBROKER.P15 // @requirement REQ-003 githubBrokerClient: createGitHubBrokerClient(), shouldUseNodePtyShell: profileSettingsWithTools.shouldUseNodePtyShell, allowPtyThemeOverride: profileSettingsWithTools.allowPtyThemeOverride, ptyScrollbackLimit: profileSettingsWithTools.ptyScrollbackLimit, enablePromptCompletion: profileSettingsWithTools.enablePromptCompletion ?? false, eventEmitter: appEvents, continueSession: argv.continue === '' || argv.continue === true ? true : (argv.continue ?? false), jitContextEnabled: context.jitContextEnabled, ...hooksConfig, }; } // ─── Main builder ───────────────────────────────────────────────────────────── /** * Constructs the Config object from all resolved values. */ export function buildConfig(input: ConfigBuildInput): Config { const { argv, profileSettingsWithTools, mcpEnabled, mcpServers, excludeTools, allowedTools, policyEngineConfig, adminSkillsEnabled, cwd, } = input; const telemetry = buildTelemetryConfig(argv, profileSettingsWithTools); const sanitizationConfig = buildSanitizationConfig(profileSettingsWithTools); const hooksConfig = buildHooksConfig( profileSettingsWithTools, adminSkillsEnabled, cwd, ); const toolConfig = buildToolConfig( argv, profileSettingsWithTools, mcpEnabled, mcpServers, excludeTools, allowedTools, policyEngineConfig, ); return new Config({ ...buildSessionBaseArgs(input, toolConfig, telemetry, sanitizationConfig), ...buildFeatureArgs(input, hooksConfig), onReloadMcpServers: input.reloadMcpServers, // @plan PLAN-20260610-ISSUE1592.P01 // @requirement REQ-INV-001, REQ-INV-002, REQ-INV-003 // The concrete factories are constructed once via the curated public // helper (createAgentRuntimeFactoryBindings) and reused here so Config // and the providers DI seam share identical bindings (#2204). agentClientFactory: agentRuntimeFactoryBindings.agentClientFactory, toolSchedulerFactory: agentRuntimeFactoryBindings.toolSchedulerFactory, taskToolRegistration: agentRuntimeFactoryBindings.taskToolRegistration(), postSkillDiscoveryToolRegistrar: registerActivateSkillTool, }); } /** * Resolves proxy URL from CLI arg or environment variables. * Priority: CLI arg > HTTPS_PROXY > https_proxy > HTTP_PROXY > http_proxy * Intentionally uses falsy coalescing (empty string should fall back to env vars). */ function resolveProxy(cliProxy: string | undefined): string | undefined { const httpsProxy = firstNonEmptyString( process.env.HTTPS_PROXY, process.env.https_proxy, ); const httpProxy = firstNonEmptyString( process.env.HTTP_PROXY, process.env.http_proxy, ); return firstNonEmptyString(cliProxy, httpsProxy, httpProxy); }