UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

217 lines 7.76 kB
/** * Update executor - performs actual package updates * Includes rollback capability */ import { execFileSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { validateUpdate } from './validator.js'; /** * audit_1776853149979: package name and version come from npm-view output and * the update-history.json file (writable by anyone with FS access). Both * previously interpolated straight into a shell string for `npm install`. * These regexes pre-flight values so a hostile package name can't slip * shell metacharacters through, even though execFileSync below already * eliminates the shell. */ // First char of the unscoped name forbids `-` to defang CLI-flag confusion // when the spec is passed to npm (npm install -evil@1.0.0 looks flag-shaped). const SAFE_PKG_RE = /^(@[a-zA-Z0-9_\-]+\/)?[a-zA-Z0-9_][a-zA-Z0-9_\-.]{0,213}$/; // semver / dist-tag / range chars only — no shell metas. const SAFE_VERSION_RE = /^[a-zA-Z0-9._\-+~^*xX]{1,64}$/; export function isSafePackageSpec(pkg, version) { return SAFE_PKG_RE.test(pkg) && SAFE_VERSION_RE.test(version); } const HISTORY_FILE = path.join(os.homedir(), '.claude-flow', 'update-history.json'); const MAX_HISTORY_ENTRIES = 100; function ensureDir() { const dir = path.dirname(HISTORY_FILE); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } export function loadHistory() { try { if (fs.existsSync(HISTORY_FILE)) { const content = fs.readFileSync(HISTORY_FILE, 'utf-8'); return JSON.parse(content); } } catch { // Corrupted file } return []; } function saveHistory(history) { ensureDir(); // Keep only last N entries const trimmed = history.slice(-MAX_HISTORY_ENTRIES); fs.writeFileSync(HISTORY_FILE, JSON.stringify(trimmed, null, 2)); } function recordUpdate(entry) { const history = loadHistory(); history.push(entry); saveHistory(history); } export async function executeUpdate(update, installedPackages, dryRun = false) { // Validate first const validation = validateUpdate(update.package, update.currentVersion, update.latestVersion, installedPackages); if (!validation.valid) { return { success: false, package: update.package, version: update.latestVersion, error: `Validation failed: ${validation.incompatibilities.join(', ')}`, validation, }; } if (dryRun) { return { success: true, package: update.package, version: update.latestVersion, validation, }; } // audit_1776853149979: validate package + version regex before any exec. if (!isSafePackageSpec(update.package, update.latestVersion)) { return { success: false, package: update.package, version: update.latestVersion, error: `Refusing to install: package or version contains disallowed characters (pkg="${update.package}", version="${update.latestVersion}")`, validation, }; } try { // audit_1776853149979: switched to execFileSync('npm', argv) — no shell, // so even if validation regressed, metas in update.package would stay // literal in the argv slot. execFileSync('npm', ['install', `${update.package}@${update.latestVersion}`, '--save-exact'], { encoding: 'utf-8', stdio: 'pipe', timeout: 60000, // 1 minute timeout shell: false, }); // Record successful update recordUpdate({ timestamp: new Date().toISOString(), package: update.package, fromVersion: update.currentVersion, toVersion: update.latestVersion, success: true, rollbackAvailable: true, }); return { success: true, package: update.package, version: update.latestVersion, validation, }; } catch (error) { const err = error; // Record failed update recordUpdate({ timestamp: new Date().toISOString(), package: update.package, fromVersion: update.currentVersion, toVersion: update.latestVersion, success: false, error: err.message, rollbackAvailable: false, }); return { success: false, package: update.package, version: update.latestVersion, error: err.message, validation, }; } } export async function executeMultipleUpdates(updates, installedPackages, dryRun = false) { const results = []; // Execute updates sequentially to avoid conflicts for (const update of updates) { const result = await executeUpdate(update, installedPackages, dryRun); results.push(result); // Update installed packages for next validation if (result.success) { installedPackages[update.package] = update.latestVersion; } // Stop on critical failures if (!result.success && update.priority === 'critical') { break; } } return results; } export async function rollbackUpdate(packageName) { const history = loadHistory(); if (history.length === 0) { return { success: false, message: 'No update history available' }; } // Find the last successful update for this package (or any if not specified) const lastUpdate = packageName ? history .reverse() .find((h) => h.package === packageName && h.success && h.rollbackAvailable) : history.reverse().find((h) => h.success && h.rollbackAvailable); if (!lastUpdate) { return { success: false, message: packageName ? `No rollback available for ${packageName}` : 'No rollback available', }; } // audit_1776853149979: history entries can be tampered with by anyone who // can write update-history.json — gate before exec. if (!isSafePackageSpec(lastUpdate.package, lastUpdate.fromVersion)) { return { success: false, message: `Refusing to rollback: package or version contains disallowed characters (pkg="${lastUpdate.package}", version="${lastUpdate.fromVersion}")`, }; } try { // execFileSync, no shell. execFileSync('npm', ['install', `${lastUpdate.package}@${lastUpdate.fromVersion}`, '--save-exact'], { encoding: 'utf-8', stdio: 'pipe', timeout: 60000, shell: false, }); // Record the rollback recordUpdate({ timestamp: new Date().toISOString(), package: lastUpdate.package, fromVersion: lastUpdate.toVersion, toVersion: lastUpdate.fromVersion, success: true, rollbackAvailable: false, // Can't rollback a rollback }); return { success: true, message: `Rolled back ${lastUpdate.package} from ${lastUpdate.toVersion} to ${lastUpdate.fromVersion}`, }; } catch (error) { const err = error; return { success: false, message: `Rollback failed: ${err.message}`, }; } } export function getUpdateHistory(limit = 20) { const history = loadHistory(); return history.slice(-limit).reverse(); } export function clearHistory() { if (fs.existsSync(HISTORY_FILE)) { fs.unlinkSync(HISTORY_FILE); } } //# sourceMappingURL=executor.js.map