import os from 'node:os'; import path from 'node:path'; import fsp from 'node:fs/promises'; import { parse as parseDotenv } from 'dotenv'; import { createQwenDocument, parseQwenDocument, parseQwenJsoncObject, qwenDocumentLabel, qwenSourceLabel, type QwenDocument, type QwenDocumentKind, } from './qwen-config.js'; import { readPhysicalFile, type FileSnapshot } from './managed-files.js'; const MAX_SOURCE_BYTES = 2 * 1024 * 1024; const HOME_ENV_KEYS = ['QWEN_HOME', 'QWEN_RUNTIME_DIR'] as const; export interface QwenSourcePrecondition { readonly filePath: string; readonly label: string; readonly maximumBytes: number; readonly original?: FileSnapshot; } export interface QwenDisableControl { readonly disabled: boolean; readonly source?: QwenDocument; } export interface QwenSources { readonly qwenHome: string; readonly systemDefaults: QwenDocument; readonly user: QwenDocument; readonly workspace: QwenDocument; readonly system: QwenDocument; readonly workspaceActive: boolean; readonly workspaceTrusted: boolean; readonly disableControl: QwenDisableControl; readonly preconditions: readonly QwenSourcePrecondition[]; } interface RoutingResult { readonly qwenHome: string; readonly preconditions: readonly QwenSourcePrecondition[]; } function defaultQwenHome(): string { const home = os.homedir(); return home ? path.join(home, '.qwen') : path.join(os.tmpdir(), '.qwen'); } function resolveConfigPath(value: string): string { let resolved = value; if (resolved === '~' || resolved.startsWith('~/') || resolved.startsWith('~\\')) { const segments = resolved === '~' ? [] : resolved.slice(2).split(/[/\\]+/).filter(Boolean); resolved = path.join(os.homedir(), ...segments); } return path.isAbsolute(resolved) ? resolved : path.resolve(resolved); } function environmentValue(key: string): { own: boolean; value?: string } { return { own: Object.hasOwn(process.env, key), value: process.env[key], }; } async function resolveQwenRouting(): Promise { const values = new Map(); const owned = new Set(); for (const key of HOME_ENV_KEYS) { const current = environmentValue(key); values.set(key, current.value); if (current.own) owned.add(key); } if (HOME_ENV_KEYS.every((key) => values.get(key))) { return { qwenHome: resolveConfigPath(values.get('QWEN_HOME')!), preconditions: [], }; } const initialQwenHome = values.get('QWEN_HOME'); const initialDirectory = initialQwenHome ? resolveConfigPath(initialQwenHome) : defaultQwenHome(); const candidates = [path.join(initialDirectory, '.env')]; if (!initialQwenHome) candidates.push(path.join(path.dirname(initialDirectory), '.env')); const preconditions: QwenSourcePrecondition[] = []; const visited = new Set(); const readCandidate = async (filePath: string): Promise => { const resolved = path.resolve(filePath); const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved; if (visited.has(key)) return; visited.add(key); const snapshot = await readPhysicalFile(resolved, 'Qwen Code home environment'); preconditions.push({ filePath: resolved, label: 'Qwen Code home environment', maximumBytes: MAX_SOURCE_BYTES, original: snapshot, }); if (!snapshot) return; const parsed = parseDotenv(snapshot.contents); for (const envKey of HOME_ENV_KEYS) { const value = parsed[envKey]; if (value && !owned.has(envKey)) { values.set(envKey, value); owned.add(envKey); } } }; for (const candidate of candidates) await readCandidate(candidate); const discoveredHome = values.get('QWEN_HOME'); if (discoveredHome && discoveredHome !== initialQwenHome) { const discoveredDirectory = resolveConfigPath(discoveredHome); if (discoveredDirectory !== initialDirectory) { await readCandidate(path.join(discoveredDirectory, '.env')); } } return { qwenHome: values.get('QWEN_HOME') ? resolveConfigPath(values.get('QWEN_HOME')!) : defaultQwenHome(), preconditions, }; } function systemSettingsPath(): string { const configured = process.env.QWEN_CODE_SYSTEM_SETTINGS_PATH; if (configured) return path.resolve(configured); if (process.platform === 'darwin') { return '/Library/Application Support/QwenCode/settings.json'; } if (process.platform === 'win32') return 'C:\\ProgramData\\qwen-code\\settings.json'; return '/etc/qwen-code/settings.json'; } function systemDefaultsPath(systemPath: string): string { const configured = process.env.QWEN_CODE_SYSTEM_DEFAULTS_PATH; return configured ? path.resolve(configured) : path.join(path.dirname(systemPath), 'system-defaults.json'); } async function readDocument( kind: QwenDocumentKind, filePath: string, ): Promise { const snapshot = await readPhysicalFile(filePath, qwenSourceLabel(kind)); return snapshot ? parseQwenDocument({ kind, exists: true, filePath, raw: snapshot.contents, snapshot, }) : createQwenDocument(kind, filePath); } async function canonicalPath(filePath: string): Promise { try { return await fsp.realpath(filePath); } catch { return path.resolve(filePath); } } function comparisonPath(filePath: string): string { const resolved = path.resolve(filePath); return process.platform === 'win32' ? resolved.toLowerCase() : resolved; } function isWithin(child: string, parent: string): boolean { const relative = path.relative(comparisonPath(parent), comparisonPath(child)); return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); } async function workspaceTrust( system: QwenDocument, user: QwenDocument, qwenHome: string, workspacePath: string, ): Promise<{ trusted: boolean; precondition?: QwenSourcePrecondition }> { const enabled = user.folderTrustEnabled ?? system.folderTrustEnabled ?? false; if (!enabled) return { trusted: true }; const configured = process.env.QWEN_CODE_TRUSTED_FOLDERS_PATH; const filePath = configured ? path.resolve(configured) : path.join(qwenHome, 'trustedFolders.json'); const snapshot = await readPhysicalFile(filePath, 'Qwen Code trusted folders'); const precondition = { filePath, label: 'Qwen Code trusted folders', maximumBytes: MAX_SOURCE_BYTES, original: snapshot, }; if (!snapshot) return { trusted: true, precondition }; const rules = parseQwenJsoncObject(snapshot.contents, `Qwen Code trusted folders at ${filePath}`); for (const [rulePath, level] of Object.entries(rules)) { if (!['TRUST_FOLDER', 'TRUST_PARENT', 'DO_NOT_TRUST'].includes(String(level))) { throw new Error(`Qwen Code trusted folders has invalid trust level for "${rulePath}"`); } } const workspace = await canonicalPath(workspacePath); for (const [rulePath, level] of Object.entries(rules)) { const canonicalRule = await canonicalPath(String(rulePath)); const trustRoot = level === 'TRUST_PARENT' ? path.dirname(canonicalRule) : canonicalRule; if ((level === 'TRUST_FOLDER' || level === 'TRUST_PARENT') && isWithin(workspace, trustRoot)) return { trusted: true, precondition }; } for (const [rulePath, level] of Object.entries(rules)) { if (level === 'DO_NOT_TRUST' && comparisonPath(workspace) === comparisonPath(await canonicalPath(rulePath))) { return { trusted: false, precondition }; } } return { trusted: true, precondition }; } function effectiveDisable( systemDefaults: QwenDocument, user: QwenDocument, workspace: QwenDocument, system: QwenDocument, useWorkspace: boolean, ): QwenDisableControl { let disabled = false; let source: QwenDocument | undefined; const documents = [systemDefaults, user, ...(useWorkspace ? [workspace] : []), system]; for (const document of documents) { if (document.disableAllHooks === undefined) continue; disabled = document.disableAllHooks; source = document; } return { disabled, source }; } function sourcePrecondition(document: QwenDocument): QwenSourcePrecondition { return { filePath: document.filePath, label: qwenDocumentLabel(document), maximumBytes: MAX_SOURCE_BYTES, original: document.snapshot, }; } function deduplicatePreconditions( values: readonly QwenSourcePrecondition[], ): QwenSourcePrecondition[] { const result = new Map(); for (const value of values) { const key = comparisonPath(value.filePath); if (!result.has(key)) result.set(key, value); } return [...result.values()]; } export async function readQwenSources(): Promise { const routing = await resolveQwenRouting(); const systemPath = systemSettingsPath(); const workspacePath = path.join(process.cwd(), '.qwen', 'settings.json'); const system = await readDocument('system', systemPath); const systemDefaults = await readDocument( 'system-defaults', systemDefaultsPath(systemPath), ); const user = await readDocument('user', path.join(routing.qwenHome, 'settings.json')); const [canonicalWorkspace, canonicalHome] = await Promise.all([ canonicalPath(process.cwd()), canonicalPath(os.homedir()), ]); const workspaceActive = comparisonPath(canonicalWorkspace) !== comparisonPath(canonicalHome); const workspace = workspaceActive ? await readDocument('workspace', workspacePath) : createQwenDocument('workspace', workspacePath); const trust = workspaceActive ? await workspaceTrust(system, user, routing.qwenHome, canonicalWorkspace) : { trusted: false as const }; const useWorkspace = workspaceActive && trust.trusted; const preconditions = deduplicatePreconditions([ ...routing.preconditions, sourcePrecondition(systemDefaults), sourcePrecondition(user), ...(workspaceActive ? [sourcePrecondition(workspace)] : []), sourcePrecondition(system), ...(trust.precondition ? [trust.precondition] : []), ]); return { qwenHome: routing.qwenHome, systemDefaults, user, workspace, system, workspaceActive, workspaceTrusted: trust.trusted, disableControl: effectiveDisable( systemDefaults, user, workspace, system, useWorkspace, ), preconditions, }; } export function requireQwenHooksEnabled(sources: QwenSources): void { if (!sources.disableControl.disabled) return; const source = sources.disableControl.source; const location = source ? `${qwenDocumentLabel(source)} at ${source.filePath}` : 'effective settings'; throw new Error(`Qwen Code hooks are disabled by disableAllHooks in ${location}`); }