diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/core-bundle.js | 727 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 31 | ||||
| -rw-r--r-- | modules/core/module-chatstudio.js | 5 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-manager.js | 15 | ||||
| -rw-r--r-- | modules/core/module-servermon.js | 5 | ||||
| -rw-r--r-- | modules/core/module-simplegui.js | 13 | ||||
| -rw-r--r-- | modules/core/module-system.js | 139 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 17 | ||||
| -rw-r--r-- | modules/core/secret-vault.js | 32 | ||||
| -rw-r--r-- | modules/core/terminal.js | 16 | ||||
| -rw-r--r-- | modules/ext/calc.js | 33 | ||||
| -rw-r--r-- | modules/ext/console.js | 15 | ||||
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 7 | ||||
| -rw-r--r-- | modules/ext/filemgr.js | 7 | ||||
| -rw-r--r-- | modules/ext/floating.js | 13 |
16 files changed, 517 insertions, 560 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 29d6fc8..cb3471a 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -933,8 +933,23 @@ window.VirtualFileSystem = VirtualFileSystem; this.terminal = terminal; this.modules = {}; this.moduleRegistry = []; + this._timers = []; // v4.5.68: central timer registry this.loadFromStorage(); } + // v4.5.68: Central timer registry — all modules should use these instead of raw setInterval + _setInterval(fn, ms) { + const id = setInterval(fn, ms); + this._timers.push(id); + return id; + } + _clearTimer(id) { + clearInterval(id); + this._timers = this._timers.filter(t => t !== id); + } + clearAllTimers() { + this._timers.forEach(id => clearInterval(id)); + this._timers = []; + } loadFromStorage() { try { const saved = localStorage.getItem('webx11_modules'); @@ -1101,7 +1116,7 @@ class OllamaManager { this.responseHooks = []; this._abortController = null; // --- AI Agent Loop Optimization Toggle --- - this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false'; + this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') === 'true'; // v4.5.66: default OFF // --- Streaming Toggle --- this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true'; // --- Validator Agent Config --- @@ -1434,6 +1449,21 @@ class OllamaManager { return false; } + // v4.5.68: Fetch with timeout — wraps fetch() with AbortController to prevent network hangs + async fetchWithTimeout(url, options = {}, timeoutMs = 30000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const resp = await fetch(url, { ...options, signal: controller.signal }); + clearTimeout(timer); + return resp; + } catch (e) { + clearTimeout(timer); + if (e.name === 'AbortError') throw new Error(`Request timed out after ${timeoutMs / 1000}s`); + throw e; + } + } + // ─── Chat (non-streaming) ──────────────────────────────────────────────── async chat(prompt, model = null) { @@ -2207,7 +2237,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -3560,6 +3590,20 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm const parts = this.security._parseArgs(cmdText); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); + // v4.5.67: Security gate for pipe stages — was completely bypassed + const cls = this.security._classify(cmdText); + if (cls.destructive && this.security.level > 0) { + this.print(`⚠️ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); + return; + } + if (cls.system && this.security.level > 0) { + this.print(`⚠️ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); + return; + } + if (!cls.known && this.security.level >= 2) { + this.print(`⚠️ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error'); + return; + } this._pipeInput = pipeInput; if (i < pipeParts.length - 1) { pipeInput = await this.capturePipeOutput(cmd, args); @@ -4767,8 +4811,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm 'doctor': 'doctor — Run all health checks, rank issues, show top 3 warnings', 'bug-report': 'bug-report — Generate a markdown bug report to /tmp/ for debugging', 'preview': 'preview <file.html> — Render an HTML file from the VFS in a preview pane', - 'factory-reset': 'factory-reset [--confirm] — Wipe all webx11_ data (VFS, config, AI profile, mindmaps, themes, all localStorage)', - 'reset-vfs': 'reset-vfs [--confirm] — Clear virtual filesystem and auto-session backups (keeps settings, modules, AI profile)', + 'reset': 'reset [mem|vfs|config|all] — Global reset command (default: mem)', + 'factory-reset': 'factory-reset — Alias for reset all', + 'reset-vfs': 'reset-vfs — Alias for reset vfs', 'logs': 'logs [n] [LEVEL] — Show last n log entries (optionally filtered by level: INFO/WARN/ERROR)', 'logrotate': 'logrotate — Rotate current.log to a dated archive (auto-cleans old)', 'health-check': 'health-check — Audit VFS size, modules, logs, trash, skills, tool failures', @@ -5056,7 +5101,13 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm }; this.commands['preview'] = (args) => this.terminal.openPreview(args); - this.commands['factory-reset'] = (args) => { + + // v4.5.64: reset — unified reset command superseding factory-reset, reset-vfs, reset-mem, config-reset + this.commands['reset'] = (args) => this._globalReset(args); + this.commands['factory-reset'] = (args) => this._globalReset(['all', ...args]); + this.commands['reset-vfs'] = (args) => this._globalReset(['vfs', ...args]); + + this.commands['factory-reset-old'] = (args) => { if (!args[0] || args[0] !== '--confirm') { this.terminal.print('⚠️ FACTORY RESET will clear all data:', 'error'); this.terminal.print(' • Filesystem (all files + directories)', 'warning'); @@ -5199,6 +5250,122 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } }; } + + // v4.5.64: Unified reset — subcommands: mem, vfs, config, all + _globalReset(args) { + const scope = (args[0] || 'mem').toLowerCase(); + const confirmed = args.includes('--confirm') || args.includes('-y'); + + const scopes = { + mem: { + title: 'AI MEMORY', + items: ['AI conversation context', 'AI patterns & guardrails', 'AI user profile', 'AI system prompt', 'VaultMind token', 'AI internal logs (tool calls, heap stats)'], + keys: ['webx11_ai_context', 'webx11_ai_config', 'webx11_ai_patterns', 'webx11_ai_system_prompt', 'webx11_user_profile', 'webx11_guardrail_policy', 'vaultmind_token'], + desc: 'Clears AI state only. VFS, settings, modules, and config are untouched.' + }, + vfs: { + title: 'VIRTUAL FILESYSTEM', + items: ['All files and directories', 'Session backups', 'VFS metadata', 'Neural VFS index'], + desc: 'Clears the VFS to empty root. Settings, modules, AI profile, and config are preserved.' + }, + config: { + title: 'CONFIGURATION', + items: ['All webx11_config_* keys', 'Ollama endpoint & model', 'Security settings', 'Terminal config', 'Font size', 'Theme'], + keys: null, // prefix-based wipe + desc: 'Resets all configuration to defaults. VFS and AI memory are untouched.' + }, + all: { + title: 'EVERYTHING', + items: ['VFS (all files)', 'AI memory & profile', 'All config & settings', 'Module registry & disabled list', 'Terminal history', 'Themes', 'Chat settings', 'Cron jobs', 'Floating elements', 'Aliases', 'Tutorial state', 'Server list', 'Orchestrator config', 'Secret vault keys', 'Neural VFS config'], + desc: 'Complete wipe. The system will reload as if first visit.' + } + }; + + const s = scopes[scope]; + if (!s) { + this.terminal.print(`Usage: reset [mem|vfs|config|all] [--confirm]`, 'warning'); + this.terminal.print(` reset mem — Clear AI memory (default)`, 'info'); + this.terminal.print(` reset vfs — Clear virtual filesystem`, 'info'); + this.terminal.print(` reset config — Reset all config to defaults`, 'info'); + this.terminal.print(` reset all — Wipe everything (nuclear)`, 'info'); + this.terminal.print(``, 'info'); + this.terminal.print(`Aliases: factory-reset = reset all, reset-vfs = reset vfs`, 'info'); + return; + } + + if (!confirmed) { + this.terminal.print(`⚠️ RESET ${s.title} will clear:`, 'error'); + s.items.forEach(item => this.terminal.print(` • ${item}`, 'warning')); + this.terminal.print(``, 'info'); + this.terminal.print(s.desc, 'info'); + this.terminal.print(``, 'info'); + this.terminal.print(`Type: reset ${scope} --confirm`, 'error'); + return; + } + + // Execute reset + if (scope === 'mem') { + this._resetMem(); + } else if (scope === 'vfs') { + this._resetVfs(); + } else if (scope === 'config') { + this._resetConfig(); + } else if (scope === 'all') { + this._resetAll(); + } + + localStorage.setItem('webx11_last_reset', Date.now().toString()); + this.terminal.print(`✅ ${s.title} reset complete. Reloading...`, 'success'); + setTimeout(() => location.reload(), 1000); + } + + _resetMem() { + const keys = ['webx11_ai_context', 'webx11_ai_config', 'webx11_ai_patterns', 'webx11_ai_system_prompt', 'webx11_user_profile', 'webx11_guardrail_policy', 'vaultmind_token']; + keys.forEach(k => localStorage.removeItem(k)); + // Also clear AI logs in VFS + try { this.terminal.fs.deleteFile('/logs/ai-tool-log.json'); } catch (e) {} + } + + _resetVfs() { + this.terminal.fs.root = { type: 'dir', content: {} }; + this.terminal.fs.currentPath = []; + try { + this.terminal.fs.deleteFile('_sessions/_auto.json'); + this.terminal.fs.deleteFile('_sessions/_auto-context.json'); + } catch (e) {} + this.terminal.fs.saveToStorage(); + } + + _resetConfig() { + // Wipe all webx11_config_* keys + const toRemove = []; + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key && (key.startsWith('webx11_config_') || key === 'webx11_ollama_endpoint' || key === 'webx11_ollama_model' || key.startsWith('webx11_security_') || key === 'webx11_terminal_config' || key === 'webx11_font_size' || key === 'webx11_active_theme' || key === 'webx11_themes' || key === 'webx11_model_params' || key.startsWith('webx11_validator_') || key === 'webx11_optimization_enabled' || key === 'webx11_stream_enabled' || key === 'webx11_smart_prompt' || key === 'webx11_config_ai_sound' || key === 'webx11_welcome_dismissed')) { + toRemove.push(key); + } + } + toRemove.forEach(k => localStorage.removeItem(k)); + } + + _resetAll() { + // Delete session backups so they don't resurrect + try { + this.terminal.fs.deleteFile('_sessions/_auto.json'); + this.terminal.fs.deleteFile('_sessions/_auto-context.json'); + this.terminal.fs.reset(); + } catch (e) { console.warn('reset all FS cleanup:', e); } + // Wipe ALL webx11_ prefixed keys + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key && key.startsWith('webx11_')) localStorage.removeItem(key); + } + // Also wipe non-prefixed keys used by modules + const extraKeys = ['vaultmind_token', 'cslite_messages', 'webx11_chat_settings', 'webx11_chat_logs', 'webx11_chat_api']; + extraKeys.forEach(k => localStorage.removeItem(k)); + this.terminal.outputContainer.innerHTML = ''; + } + _autoRotateLogs(force = false) { try { const content = this.terminal.fs.readFile('/logs/current.log') || ''; @@ -5321,7 +5488,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm FileSystemModule: '📁 Filesystem', WebX11AIAssistantModule: '🤖 AI', WebXAIAliasModule: '🌐 WebX', - DuckDuckGoModule: '🔍 Search & Web', + SearXNGModule: '🔍 Search & Web', WebFetcherModule: '🔍 Search & Web', AliasModule: '🔧 Tools', @@ -5346,18 +5513,18 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm NeuralVFSModule: '🧠 Neural VFS', SkillPackCoreModule: '📚 Skills', ConfigManagerModule: '⚙️ Config', - ModuleIOModule: '🔌 I/O', + SandboxModule: '🏖️ Sandbox', - MyModule: '💾 Data', + WebDAVModule: '🧰 Extras', ChatStudioModule: '🧰 Extras', SimpleGUIModule: '🖥️ GUI', SecretVault: '🔒 Vault', OrchestratorModule: '🧠 Orchestrator', PredictorModule: '🔮 Prediction', - SmartContextModule: '🧠 Context', + MemoryGraphModule: '📊 Memory', - DynamicPromptsModule: '💬 Prompts', + WorldModelModule: '🌍 World', LeaseModule: '🔐 Leases', AgentRegistryModule: '🤖 Agents', @@ -6118,90 +6285,6 @@ class OllamaModule { } } -/* === module-my.js === */ -class MyModule { - constructor() { - this.commands = {}; this.terminal = null; this.userData = {}; - try { const d = localStorage.getItem('webx11_my_data'); if (d) this.userData = JSON.parse(d); } catch (e) {} - } - getHelpInfo() { - return { - 'my': 'my — Show module info and stored key count', - 'my-set': 'my-set <key> <value> — Store a key-value pair', - 'my-get': 'my-get <key> — Retrieve a stored value by key', - 'my-list': 'my-list — List all stored keys', - 'my-export': 'my-export — Export all stored data as JSON', - 'my-import': 'my-import <json> — Import data from JSON string', - 'my-clear': 'my-clear — Clear all stored data' - }; - } - _save() { - try { localStorage.setItem('webx11_my_data', JSON.stringify(this.userData)); } catch (e) {} - } - registerCommands() { - this.commands['my'] = () => this.showInfo(); - this.commands['my-set'] = (args) => this.setData(args); - this.commands['my-get'] = (args) => this.getData(args); - this.commands['my-list'] = () => this.listData(); - this.commands['my-export'] = () => this.exportData(); - this.commands['my-import'] = (args) => this.importData(args); - this.commands['my-clear'] = () => this.clearData(); - } - showInfo() { - this.terminal.print("=== My Module v2.0 ===", "info"); - this.terminal.print("Personal workspace for custom commands"); - this.terminal.print(`Stored keys: ${Object.keys(this.userData).length}`, "success"); - } - setData(args) { - if (args.length < 2) { - this.terminal.print("Usage: my-set <key> <value>", "warning"); return; - } - const key = args[0], value = args.slice(1).join(' '); - this.userData[key] = { value, timestamp: new Date().toISOString() }; - this._save(); - this.terminal.print(`Saved: ${key} = ${value}`, "success"); - } - getData(args) { - if (!args[0]) { - this.terminal.print("Usage: my-get <key>", "warning"); return; - } - if (this.userData[args[0]]) { - this.terminal.print(`${args[0]}: ${this.userData[args[0]].value}`, "info"); - } else { - this.terminal.print(`Key '${args[0]}' not found`, "error"); - } - } - listData() { - const keys = Object.keys(this.userData); - if (!keys.length) return this.terminal.print('No data stored.', 'info'); - this.terminal.print('=== My Data ===', 'info'); - keys.forEach(k => { - const v = this.userData[k]; - const type = typeof v.value; - this.terminal.print(` ${k} (${type}): ${String(v.value).substring(0, 40)}`); - }); - } - exportData() { - this.terminal.print(JSON.stringify(this.userData, null, 2)); - } - importData(args) { - if (!args[0]) return this.terminal.print('Usage: my-import <json>', 'warning'); - try { - const data = JSON.parse(args.join(' ')); - Object.assign(this.userData, data); - this._save(); - this.terminal.print(`Imported ${Object.keys(data).length} keys`, 'success'); - } catch (e) { - this.terminal.print('Invalid JSON: ' + e.message, 'error'); - } - } - clearData() { - this.userData = {}; - localStorage.removeItem('webx11_my_data'); - this.terminal.print("Personal data cleared", "success"); - } -} - /* === module-modmgr.js === */ // Extracted from newOS4 index.html — ModuleManagerModule // v4.5.43: Enhanced with module-info, module-list, module-stats, module-disable/enable, @@ -6781,9 +6864,10 @@ class ServerMonitorModule { if (this._latencyLog.length > this._maxLatencyEntries) { this._latencyLog = this._latencyLog.slice(-this._maxLatencyEntries); } - // Persist every 20 calls - if (this._latencyLog.length % 20 === 0) { + // v4.5.66: Throttled persistence — every 100 calls or 5 minutes (was every 20) + if (this._latencyLog.length % 100 === 0 || (Date.now() - (this._lastPersistTs || 0)) > 300000) { this._persistLatency(); + this._lastPersistTs = Date.now(); } } _persistLatency() { @@ -7086,13 +7170,22 @@ class ServerMonitorModule { } const label = args[0], command = args.slice(1).join(' '); const id = 'gui-btn-' + Date.now(); - const safeCmd = command.replace(/'/g, "\\'"); + // v4.5.68: Use addEventListener instead of onclick attribute to prevent quote-breaking XSS this.terminal.printHTML(`<div style="margin: 5px 0;"> - <button class="qa-btn" onclick="(window.webx11||window).executeCommand('${safeCmd}')" id="${id}">${label}</button> + <button class="qa-btn" id="${id}">${this._escapeHtml(label)}</button> </div>`); + const btn = document.getElementById(id); + if (btn) { + btn.addEventListener('click', () => { + (window.webx11 || window).executeCommand(command); + }); + } this.guiElements[id] = { type: 'button', label, command }; this.terminal.print(`Button '${label}' created`, "success"); } + _escapeHtml(str) { + return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); + } createInput(args) { if (args.length < 2) { this.terminal.print("Usage: gui-input <placeholder> <varName>", "warning"); return; @@ -7124,192 +7217,6 @@ class ServerMonitorModule { } } -/* === module-io.js === */ -// Extracted from newOS4 index.html — ModuleIOModule - class ModuleIOModule { - constructor() { this.commands = {}; this.terminal = null; } - getHelpInfo() { - return { - 'export-mods': 'export-mods — Export all loaded modules to a JSON file', - 'import-mods': 'import-mods <filename> — Import modules from a JSON file', - 'backup': 'backup — Create a full system backup', - 'restore': 'restore <filename> — Restore system from a backup file', - 'share-mod': 'share-mod <module-name> — Share a module as JSON to clipboard' - }; - } - registerCommands() { - this.commands['export-mods'] = () => this.exportModules(); - this.commands['import-mods'] = (args) => this.importModules(args); - this.commands['backup'] = () => this.fullBackup(); - this.commands['restore'] = (args) => this.restoreBackup(args); - this.commands['share-mod'] = (args) => this.shareModule(args); - } - exportModules() { - const registry = this.terminal.moduleManager.moduleRegistry; - if (registry.length === 0) { - this.terminal.print("No modules to export", "warning"); return; - } - const exportData = { version: '4.1', exported: new Date().toISOString(), modules: registry }; - const json = JSON.stringify(exportData, null, 2); - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; a.download = `webx11-modules-${Date.now()}.json`; a.click(); - URL.revokeObjectURL(url); - this.terminal.print(`Exported ${registry.length} module(s)`, "success"); - } - importModules(args) { - this.terminal.print("⚠️ To import modules:", "warning"); - this.terminal.print("1. Upload the JSON file using 'upload'", "info"); - this.terminal.print("2. Use: import-mods <filename>", "info"); - } - async fullBackup() { - const backup = { - version: '4.1', timestamp: new Date().toISOString(), - filesystem: localStorage.getItem('webx11_filesystem'), - modules: localStorage.getItem('webx11_modules'), - ollama: { - endpoint: localStorage.getItem('webx11_ollama_endpoint'), - model: localStorage.getItem('webx11_ollama_model') - }, config: {} - }; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key.startsWith('webx11_')) { backup.config[key] = localStorage.getItem(key); } - } - const json = JSON.stringify(backup, null, 2); - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; a.download = `webx11-backup-${Date.now()}.json`; a.click(); - URL.revokeObjectURL(url); - this.terminal.print("Full backup created", "success"); - } - restoreBackup(args) { - this.terminal.print("⚠️ To restore backup:", "warning"); - this.terminal.print("1. Upload the backup JSON file", "info"); - this.terminal.print("2. Use: restore <filename>", "info"); - } - shareModule(args) { - if (!args[0]) { - this.terminal.print("Usage: share-mod <module-name>", "warning"); return; - } - const mod = this.terminal.moduleManager.moduleRegistry.find(m => m.name === args[0]); - if (!mod) { - this.terminal.print(`Module '${args[0]}' not found`, "error"); return; - } - const shareData = { name: mod.name, url: mod.url, shared: new Date().toISOString() }; - const json = JSON.stringify(shareData, null, 2); - navigator.clipboard.writeText(json).then(() => { - this.terminal.print("Module info copied to clipboard", "success"); - }).catch(() => { this.terminal.print(json, "info"); }); - } - } - -/* === module-dynamicprompts.js === */ -// Extracted from newOS4 index.html — DynamicPromptsModule - class DynamicPromptsModule { - constructor() { - this.commands = {}; this.terminal = null; - this.prompts = this.loadPrompts(); this.variables = {}; - } - getHelpInfo() { - return { - 'prompt': 'prompt <name> [additional-text] — Use a saved prompt template', - 'prompt-save': 'prompt-save <name> <template> — Save a new prompt template', - 'prompt-list': 'prompt-list — List all saved prompt templates', - 'prompt-delete': 'prompt-delete <name> — Delete a saved prompt', - 'prompt-var': 'prompt-var <name> <value> — Set a template variable', - 'prompt-vars': 'prompt-vars — List all template variables' - }; - } - loadPrompts() { - try { - const saved = localStorage.getItem('webx11_prompts'); - return saved ? JSON.parse(saved) : {}; - } catch (e) { return {}; } - } - savePrompts() { - localStorage.setItem('webx11_prompts', JSON.stringify(this.prompts)); - } - registerCommands() { - this.commands['prompt'] = (args) => this.usePrompt(args); - this.commands['prompt-save'] = (args) => this.savePrompt(args); - this.commands['prompt-list'] = () => this.listPrompts(); - this.commands['prompt-delete'] = (args) => this.deletePrompt(args); - this.commands['prompt-var'] = (args) => this.setVariable(args); - this.commands['prompt-vars'] = () => this.listVariables(); - } - usePrompt(args) { - if (!args[0]) { - this.terminal.print("Usage: prompt <name> [additional-text]", "warning"); return; - } - const name = args[0], additional = args.slice(1).join(' '); - if (!this.prompts[name]) { - this.terminal.print(`Prompt '${name}' not found`, "error"); return; - } - let template = this.prompts[name].template; - Object.entries(this.variables).forEach(([k, v]) => { - template = template.replace(new RegExp(`\\{${k}\\}`, 'g'), v); - }); - const finalPrompt = additional ? `${template} ${additional}` : template; - this.terminal.print("=== Generated Prompt ===", "info"); - this.terminal.printHTML(`<div class="output-line ollama" style="margin-top: 5px;"> - <button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.innerText.replace('Copy','').trim())">Copy</button> - ${finalPrompt} - </div>`); - this.terminal.print("\nUse with: ai <generated-prompt>", "info"); - } - savePrompt(args) { - if (args.length < 2) { - this.terminal.print("Usage: prompt-save <name> <template>", "warning"); return; - } - const name = args[0], template = args.slice(1).join(' '); - this.prompts[name] = { template, created: new Date().toISOString() }; - this.savePrompts(); - this.terminal.print(`Prompt '${name}' saved`, "success"); - } - listPrompts() { - const names = Object.keys(this.prompts); - if (names.length === 0) { - this.terminal.print("No saved prompts", "info"); return; - } - this.terminal.print("=== Saved Prompts ===", "info"); - names.forEach(name => { - const p = this.prompts[name]; - this.terminal.print(`📝 ${name}: ${p.template.substring(0, 50)}...`); - }); - } - deletePrompt(args) { - if (!args[0]) { - this.terminal.print("Usage: prompt-delete <name>", "warning"); return; - } - if (this.prompts[args[0]]) { - delete this.prompts[args[0]]; - this.savePrompts(); - this.terminal.print(`Prompt '${args[0]}' deleted`, "success"); - } else { - this.terminal.print(`Prompt '${args[0]}' not found`, "error"); - } - } - setVariable(args) { - if (args.length < 2) { - this.terminal.print("Usage: prompt-var <name> <value>", "warning"); return; - } - const name = args[0], value = args.slice(1).join(' '); - this.variables[name] = value; - this.terminal.print(`Variable set: ${name} = ${value}`, "success"); - } - listVariables() { - const names = Object.keys(this.variables); - if (names.length === 0) { - this.terminal.print("No variables set", "info"); return; - } - this.terminal.print("=== Variables ===", "info"); - names.forEach(name => { this.terminal.print(`${name}: ${this.variables[name]}`); }); - } - } - /* === module-aiassist.js === */ // Extracted from newOS4 index.html — WebX11AIAssistantModule (Full Agent Loop) class WebX11AIAssistantModule { @@ -7365,9 +7272,10 @@ buildSystemPrompt() { const saved = localStorage.getItem('webx11_ai_system_prompt'); if (saved) { const now = new Date(); - const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + const dayStr = now.toLocaleDateString('en-US', { weekday: 'long' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); - return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\n${saved}`; + return `Current date: ${dateStr} (${dayStr}) ${timeStr}\n\n${saved}`; } } catch (e) {} // Check SmartContextModule for active profile system prompt @@ -7380,6 +7288,8 @@ buildSystemPrompt() { } catch (e) {} const now = new Date(); const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + const dayStr = now.toLocaleDateString('en-US', { weekday: 'long' }); + const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; let nvfsNote = ''; try { @@ -7388,7 +7298,7 @@ buildSystemPrompt() { if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } catch (e) {} - let base = 'Current date: ' + dateStr + nvfsNote + '\n' + let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${nvfsNote}\n` + 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n' + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' @@ -8070,6 +7980,7 @@ buildSystemPrompt() { } async agenticLoop(messages) { let toolTurns = 0; + const _seenCalls = new Map(); // v4.5.66: loop detection — track repeated tool calls while (toolTurns < this.MAX_TOOL_TURNS) { if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } toolTurns++; @@ -8091,6 +8002,23 @@ buildSystemPrompt() { return { finalAnswer: text, toolTurns }; } if (res.tool_calls && res.tool_calls.length > 0) { + // v4.5.66: Loop detection — break if AI repeats same tool+args 3+ times + let looping = false; + for (const tc of res.tool_calls) { + const sig = tc.function.name + ':' + (tc.function.arguments || ''); + const count = (_seenCalls.get(sig) || 0) + 1; + _seenCalls.set(sig, count); + if (count >= 3) { + this.terminal.print(`🔄 Loop detected: ${tc.function.name} called ${count}x with same args — breaking loop`, 'warning'); + looping = true; + break; + } + } + if (looping) { + messages.push(res.message); + messages.push({ role: 'system', content: 'You are repeating the same tool call with identical arguments. Stop calling tools and provide your final answer based on what you already know.' }); + continue; + } this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info'); messages.push(res.message); let pendingText = res.message?.content || null; @@ -8739,7 +8667,7 @@ buildSystemPrompt() { 'mem-debug','reset-mem']); return !safeRead.has(cmd); } - return ['write_file','delete_file','make_directory','edit_module'].includes(name); + return ['write_file','delete_file','make_directory','edit_module','delegate_task','spawn_agent','create_skill','patch_skill','wpm_install','user_set','session_save'].includes(name); } async runTool(name, argsStr) { let a = {}; @@ -9669,7 +9597,7 @@ buildSystemPrompt() { this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, ''); - this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled !== false) ? 'on' : 'off'}`, ''); + this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled) ? 'on' : 'off (default)'}`, ''); this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); @@ -9956,190 +9884,6 @@ buildSystemPrompt() { } } -/* === module-smartctx.js === */ -// Extracted from newOS4 index.html — SmartContextModule - class SmartContextModule { - constructor() { - this.commands = {}; this.terminal = null; - this.contextProfiles = this.loadProfiles(); this.activeProfile = this.loadActiveProfile(); - } - getHelpInfo() { - return { - 'context': 'context [name] — Set or view current context profile', - 'contexts': 'contexts — List all available context profiles', - 'context-new': 'context-new <name> <displayName> <systemPrompt> — Create a new context profile', - 'context-delete': 'context-delete <name> — Delete a context profile', - 'smart-ask': 'smart-ask <question> — Ask a question using the active context' - }; - } - loadProfiles() { - try { - const saved = localStorage.getItem('webx11_context_profiles'); - return saved ? JSON.parse(saved) : this.getDefaultProfiles(); - } catch (e) { return this.getDefaultProfiles(); } - } - saveProfiles() { - localStorage.setItem('webx11_context_profiles', JSON.stringify(this.contextProfiles)); - } - loadActiveProfile() { - try { - const saved = localStorage.getItem('webx11_active_context_profile'); - if (saved && this.contextProfiles[saved]) return saved; - } catch (e) {} - return null; - } - saveActiveProfile() { - try { - if (this.activeProfile) { - localStorage.setItem('webx11_active_context_profile', this.activeProfile); - } else { - localStorage.removeItem('webx11_active_context_profile'); - } - } catch (e) {} - } - getDefaultProfiles() { - return { - 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 }, - 'debug': { name: 'Debug Helper', systemPrompt: "You are a debugging expert.", temperature: 0.5 }, - 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative writer.", temperature: 0.9 }, - 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical writer.", temperature: 0.3 }, - 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly assistant.", temperature: 0.8 } - }; - } - registerCommands() { - this.commands['context'] = (args) => this.setContext(args); - this.commands['contexts'] = () => this.listContexts(); - this.commands['context-new'] = (args) => this.createContext(args); - this.commands['context-delete'] = (args) => this.deleteContext(args); - this.commands['smart-ask'] = (args) => this.smartAsk(args); - } - setContext(args) { - if (!args[0]) { - this.terminal.print(`Current context: ${this.activeProfile || 'none'}`, "info"); return; - } - if (this.contextProfiles[args[0]]) { - this.activeProfile = args[0]; - this.saveActiveProfile(); - this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success"); - } else { - this.terminal.print(`Profile '${args[0]}' not found`, "error"); - } - } - listContexts() { - this.terminal.print("=== Context Profiles ===", "info"); - Object.entries(this.contextProfiles).forEach(([key, profile]) => { - const active = key === this.activeProfile ? ' ✅' : ''; - this.terminal.print(`${key}: ${profile.name}${active}`); - }); - } - createContext(args) { - if (args.length < 3) { - this.terminal.print("Usage: context-new <name> <displayName> <systemPrompt>", "warning"); return; - } - const name = args[0], displayName = args[1], systemPrompt = args.slice(2).join(' '); - this.contextProfiles[name] = { name: displayName, systemPrompt, temperature: 0.7 }; - this.saveProfiles(); - this.terminal.print(`Context profile '${name}' created`, "success"); - } - deleteContext(args) { - if (!args[0]) { - this.terminal.print("Usage: context-delete <name>", "warning"); return; - } - if (this.contextProfiles[args[0]]) { - delete this.contextProfiles[args[0]]; - this.saveProfiles(); - if (this.activeProfile === args[0]) { this.activeProfile = null; } - this.terminal.print(`Context profile '${args[0]}' deleted`, "success"); - } else { - this.terminal.print(`Profile '${args[0]}' not found`, "error"); - } - } - async smartAsk(args) { - if (!args[0]) { - this.terminal.print("Usage: smart-ask <your-question>", "warning"); return; - } - let systemPrompt = "You are a helpful assistant."; - let temperature = 0.7; - if (this.activeProfile && this.contextProfiles[this.activeProfile]) { - const profile = this.contextProfiles[this.activeProfile]; - systemPrompt = profile.systemPrompt; - temperature = profile.temperature || 0.7; - this.terminal.print(`Using context: ${profile.name} (temp: ${temperature})`, "info"); - } - // Use agentic loop via the AI assistant module for tool-enabled responses - const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule; - if (aiModule) { - // Set temperature on ollamaManager modelParams temporarily - const om = this.terminal.ollamaManager; - const savedTemp = om.modelParams ? om.modelParams.temperature : undefined; - if (om.modelParams) { - om.modelParams.temperature = temperature; - } - try { - const messages = [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: args.join(' ') } - ]; - const result = await om.chatWithTools(messages, aiModule.getToolDefinitions()); - if (result && result.message) { - this.terminal.printAIResponse(result.message.content || '(no response)'); - } else if (result && result.error) { - this.terminal.print(`⚠️ AI error: ${result.error}`, 'error'); - } - } finally { - if (om.modelParams && savedTemp !== undefined) { - om.modelParams.temperature = savedTemp; - } - } - } else { - // Fallback: simple chat with temperature - const om = this.terminal.ollamaManager; - const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`; - await om.chat(fullPrompt); - } - } - } - -/* === module-duckduckgo.js === */ -// Extracted from newOS4 index.html — DuckDuckGoModule - class DuckDuckGoModule { - constructor() { - this.commands = {}; - this.terminal = null; - } - getHelpInfo() { - return { - 'ddg': 'ddg <query> — Search DuckDuckGo (opens query in new tab)', - 'duck': 'duck <query> — Alias for ddg', - }; - } - registerCommands() { - this.commands['ddg'] = (args) => this.search(args); - this.commands['duck'] = (args) => this.search(args); - } - async search(args) { - if (!args[0]) { - this.terminal.print("Usage: ddg <search-query>", "warning"); - this.terminal.print("Example: ddg javascript tutorials", "info"); - this.terminal.print("Note: Due to CORS, results may be limited. Try 'search' command for better results.", "warning"); - return; - } - const query = args.join(' '); - this.terminal.print(`🦆 DuckDuckGo Search: ${query}`, "info"); - - // DuckDuckGo doesn't have a public API, so we provide alternatives - this.terminal.print("⚠️ DuckDuckGo doesn't provide a public API for direct access.", "warning"); - this.terminal.print("Opening search in new tab...", "info"); - - // Open in new tab as fallback - const ddgUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`; - window.open(ddgUrl, '_blank'); - - this.terminal.print(`\n💡 Alternative: Use 'search <query>' for SearXNG (privacy-focused)`, "info"); - this.terminal.print(`🔗 Direct link: ${ddgUrl}`, "info"); - } - } - /* === module-fetcher.js === */ class WebFetcherModule { constructor() { @@ -10414,38 +10158,20 @@ class SecretVault { } /** - * Scans text for any stored secret values and replaces them with '[REDACTED]'. - * Used by terminal and AI context to prevent leaking secrets in output. + * v4.5.68: Redacts known secret NAMES (not values) from text — sync, no decryption needed. + * Previous version decrypted ALL secrets on every call (perf + side-channel + failed when locked). + * Now just redacts the secret names if they appear in output. Values are never decrypted into memory. */ - async redactSecret(text) { + redactSecret(text) { if (!text || typeof text !== 'string') return text; - const names = this.list(); if (names.length === 0) return text; - let redacted = text; - for (const name of names) { - try { - const value = await this.decrypt(name); - if (value && typeof value === 'string' && value.length > 0) { - // Escape special regex characters in the secret value - const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - try { - const regex = new RegExp(escaped, 'g'); - redacted = redacted.replace(regex, '[REDACTED]'); - } catch (e) { - // If regex fails (e.g., complex Unicode), try manual replacement - while (redacted.includes(value)) { - redacted = redacted.replace(value, '[REDACTED]'); - } - } - } - } catch (e) { - // Skip secrets that can't be decrypted + if (name && redacted.includes(name)) { + redacted = redacted.split(name).join('[REDACTED]'); } } - return redacted; } @@ -10490,7 +10216,7 @@ class SecretVault { { name: 'PBKDF2', salt: salt, - iterations: 100000, + iterations: 600000, // v4.5.68: 100k → 600k (OWASP 2023 recommendation) hash: 'SHA-256', }, keyMaterial, @@ -10786,7 +10512,7 @@ class ChatStudioModule { window.b64_to_utf8 = function(str) { return decodeURIComponent(escape(atob(str))); }; window.escapeHtml = function(text) { if (typeof text !== 'string') return ''; - return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"'); + return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\\\"/g, '"').replace(/'/g, '''); }; window.generateId = function() { return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); }; window.showToast = function(msg) { @@ -10978,7 +10704,8 @@ class ChatStudioModule { .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') - .replace(/"/g, '"'); + .replace(/"/g, '"') + .replace(/'/g, '''); } function utf8_to_b64(str) { diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 3cf8b65..ee36789 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -52,9 +52,10 @@ buildSystemPrompt() { const saved = localStorage.getItem('webx11_ai_system_prompt'); if (saved) { const now = new Date(); - const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + const dayStr = now.toLocaleDateString('en-US', { weekday: 'long' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); - return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\n${saved}`; + return `Current date: ${dateStr} (${dayStr}) ${timeStr}\n\n${saved}`; } } catch (e) {} // Check SmartContextModule for active profile system prompt @@ -67,6 +68,8 @@ buildSystemPrompt() { } catch (e) {} const now = new Date(); const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); + const dayStr = now.toLocaleDateString('en-US', { weekday: 'long' }); + const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; let nvfsNote = ''; try { @@ -75,7 +78,7 @@ buildSystemPrompt() { if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } catch (e) {} - let base = 'Current date: ' + dateStr + nvfsNote + '\n' + let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${nvfsNote}\n` + 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n' + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' @@ -757,6 +760,7 @@ buildSystemPrompt() { } async agenticLoop(messages) { let toolTurns = 0; + const _seenCalls = new Map(); // v4.5.66: loop detection — track repeated tool calls while (toolTurns < this.MAX_TOOL_TURNS) { if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } toolTurns++; @@ -778,6 +782,23 @@ buildSystemPrompt() { return { finalAnswer: text, toolTurns }; } if (res.tool_calls && res.tool_calls.length > 0) { + // v4.5.66: Loop detection — break if AI repeats same tool+args 3+ times + let looping = false; + for (const tc of res.tool_calls) { + const sig = tc.function.name + ':' + (tc.function.arguments || ''); + const count = (_seenCalls.get(sig) || 0) + 1; + _seenCalls.set(sig, count); + if (count >= 3) { + this.terminal.print(`🔄 Loop detected: ${tc.function.name} called ${count}x with same args — breaking loop`, 'warning'); + looping = true; + break; + } + } + if (looping) { + messages.push(res.message); + messages.push({ role: 'system', content: 'You are repeating the same tool call with identical arguments. Stop calling tools and provide your final answer based on what you already know.' }); + continue; + } this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info'); messages.push(res.message); let pendingText = res.message?.content || null; @@ -1426,7 +1447,7 @@ buildSystemPrompt() { 'mem-debug','reset-mem']); return !safeRead.has(cmd); } - return ['write_file','delete_file','make_directory','edit_module'].includes(name); + return ['write_file','delete_file','make_directory','edit_module','delegate_task','spawn_agent','create_skill','patch_skill','wpm_install','user_set','session_save'].includes(name); } async runTool(name, argsStr) { let a = {}; @@ -2356,7 +2377,7 @@ buildSystemPrompt() { this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, ''); - this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled !== false) ? 'on' : 'off'}`, ''); + this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled) ? 'on' : 'off (default)'}`, ''); this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js index 1ca5220..d558445 100644 --- a/modules/core/module-chatstudio.js +++ b/modules/core/module-chatstudio.js @@ -65,7 +65,7 @@ class ChatStudioModule { window.b64_to_utf8 = function(str) { return decodeURIComponent(escape(atob(str))); }; window.escapeHtml = function(text) { if (typeof text !== 'string') return ''; - return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"'); + return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\\\"/g, '"').replace(/'/g, '''); }; window.generateId = function() { return Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9); }; window.showToast = function(msg) { @@ -257,7 +257,8 @@ class ChatStudioModule { .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') - .replace(/"/g, '"'); + .replace(/"/g, '"') + .replace(/'/g, '''); } function utf8_to_b64(str) { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index cfd8a68..3b33a95 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = '2026-06-17-v4.5.63'; + const LATEST_VER = '2026-06-18-v4.5.70'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-manager.js b/modules/core/module-manager.js index 4f004ee..8cecc4d 100644 --- a/modules/core/module-manager.js +++ b/modules/core/module-manager.js @@ -4,8 +4,23 @@ this.terminal = terminal; this.modules = {}; this.moduleRegistry = []; + this._timers = []; // v4.5.68: central timer registry this.loadFromStorage(); } + // v4.5.68: Central timer registry — all modules should use these instead of raw setInterval + _setInterval(fn, ms) { + const id = setInterval(fn, ms); + this._timers.push(id); + return id; + } + _clearTimer(id) { + clearInterval(id); + this._timers = this._timers.filter(t => t !== id); + } + clearAllTimers() { + this._timers.forEach(id => clearInterval(id)); + this._timers = []; + } loadFromStorage() { try { const saved = localStorage.getItem('webx11_modules'); diff --git a/modules/core/module-servermon.js b/modules/core/module-servermon.js index b4f6057..600618a 100644 --- a/modules/core/module-servermon.js +++ b/modules/core/module-servermon.js @@ -70,9 +70,10 @@ class ServerMonitorModule { if (this._latencyLog.length > this._maxLatencyEntries) { this._latencyLog = this._latencyLog.slice(-this._maxLatencyEntries); } - // Persist every 20 calls - if (this._latencyLog.length % 20 === 0) { + // v4.5.66: Throttled persistence — every 100 calls or 5 minutes (was every 20) + if (this._latencyLog.length % 100 === 0 || (Date.now() - (this._lastPersistTs || 0)) > 300000) { this._persistLatency(); + this._lastPersistTs = Date.now(); } } _persistLatency() { diff --git a/modules/core/module-simplegui.js b/modules/core/module-simplegui.js index 09d172a..bdd5f66 100644 --- a/modules/core/module-simplegui.js +++ b/modules/core/module-simplegui.js @@ -29,13 +29,22 @@ } const label = args[0], command = args.slice(1).join(' '); const id = 'gui-btn-' + Date.now(); - const safeCmd = command.replace(/'/g, "\\'"); + // v4.5.68: Use addEventListener instead of onclick attribute to prevent quote-breaking XSS this.terminal.printHTML(`<div style="margin: 5px 0;"> - <button class="qa-btn" onclick="(window.webx11||window).executeCommand('${safeCmd}')" id="${id}">${label}</button> + <button class="qa-btn" id="${id}">${this._escapeHtml(label)}</button> </div>`); + const btn = document.getElementById(id); + if (btn) { + btn.addEventListener('click', () => { + (window.webx11 || window).executeCommand(command); + }); + } this.guiElements[id] = { type: 'button', label, command }; this.terminal.print(`Button '${label}' created`, "success"); } + _escapeHtml(str) { + return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); + } createInput(args) { if (args.length < 2) { this.terminal.print("Usage: gui-input <placeholder> <varName>", "warning"); return; diff --git a/modules/core/module-system.js b/modules/core/module-system.js index f7da981..9dee38c 100644 --- a/modules/core/module-system.js +++ b/modules/core/module-system.js @@ -16,8 +16,9 @@ 'doctor': 'doctor — Run all health checks, rank issues, show top 3 warnings', 'bug-report': 'bug-report — Generate a markdown bug report to /tmp/ for debugging', 'preview': 'preview <file.html> — Render an HTML file from the VFS in a preview pane', - 'factory-reset': 'factory-reset [--confirm] — Wipe all webx11_ data (VFS, config, AI profile, mindmaps, themes, all localStorage)', - 'reset-vfs': 'reset-vfs [--confirm] — Clear virtual filesystem and auto-session backups (keeps settings, modules, AI profile)', + 'reset': 'reset [mem|vfs|config|all] — Global reset command (default: mem)', + 'factory-reset': 'factory-reset — Alias for reset all', + 'reset-vfs': 'reset-vfs — Alias for reset vfs', 'logs': 'logs [n] [LEVEL] — Show last n log entries (optionally filtered by level: INFO/WARN/ERROR)', 'logrotate': 'logrotate — Rotate current.log to a dated archive (auto-cleans old)', 'health-check': 'health-check — Audit VFS size, modules, logs, trash, skills, tool failures', @@ -305,7 +306,13 @@ }; this.commands['preview'] = (args) => this.terminal.openPreview(args); - this.commands['factory-reset'] = (args) => { + + // v4.5.64: reset — unified reset command superseding factory-reset, reset-vfs, reset-mem, config-reset + this.commands['reset'] = (args) => this._globalReset(args); + this.commands['factory-reset'] = (args) => this._globalReset(['all', ...args]); + this.commands['reset-vfs'] = (args) => this._globalReset(['vfs', ...args]); + + this.commands['factory-reset-old'] = (args) => { if (!args[0] || args[0] !== '--confirm') { this.terminal.print('⚠️ FACTORY RESET will clear all data:', 'error'); this.terminal.print(' • Filesystem (all files + directories)', 'warning'); @@ -448,6 +455,122 @@ } }; } + + // v4.5.64: Unified reset — subcommands: mem, vfs, config, all + _globalReset(args) { + const scope = (args[0] || 'mem').toLowerCase(); + const confirmed = args.includes('--confirm') || args.includes('-y'); + + const scopes = { + mem: { + title: 'AI MEMORY', + items: ['AI conversation context', 'AI patterns & guardrails', 'AI user profile', 'AI system prompt', 'VaultMind token', 'AI internal logs (tool calls, heap stats)'], + keys: ['webx11_ai_context', 'webx11_ai_config', 'webx11_ai_patterns', 'webx11_ai_system_prompt', 'webx11_user_profile', 'webx11_guardrail_policy', 'vaultmind_token'], + desc: 'Clears AI state only. VFS, settings, modules, and config are untouched.' + }, + vfs: { + title: 'VIRTUAL FILESYSTEM', + items: ['All files and directories', 'Session backups', 'VFS metadata', 'Neural VFS index'], + desc: 'Clears the VFS to empty root. Settings, modules, AI profile, and config are preserved.' + }, + config: { + title: 'CONFIGURATION', + items: ['All webx11_config_* keys', 'Ollama endpoint & model', 'Security settings', 'Terminal config', 'Font size', 'Theme'], + keys: null, // prefix-based wipe + desc: 'Resets all configuration to defaults. VFS and AI memory are untouched.' + }, + all: { + title: 'EVERYTHING', + items: ['VFS (all files)', 'AI memory & profile', 'All config & settings', 'Module registry & disabled list', 'Terminal history', 'Themes', 'Chat settings', 'Cron jobs', 'Floating elements', 'Aliases', 'Tutorial state', 'Server list', 'Orchestrator config', 'Secret vault keys', 'Neural VFS config'], + desc: 'Complete wipe. The system will reload as if first visit.' + } + }; + + const s = scopes[scope]; + if (!s) { + this.terminal.print(`Usage: reset [mem|vfs|config|all] [--confirm]`, 'warning'); + this.terminal.print(` reset mem — Clear AI memory (default)`, 'info'); + this.terminal.print(` reset vfs — Clear virtual filesystem`, 'info'); + this.terminal.print(` reset config — Reset all config to defaults`, 'info'); + this.terminal.print(` reset all — Wipe everything (nuclear)`, 'info'); + this.terminal.print(``, 'info'); + this.terminal.print(`Aliases: factory-reset = reset all, reset-vfs = reset vfs`, 'info'); + return; + } + + if (!confirmed) { + this.terminal.print(`⚠️ RESET ${s.title} will clear:`, 'error'); + s.items.forEach(item => this.terminal.print(` • ${item}`, 'warning')); + this.terminal.print(``, 'info'); + this.terminal.print(s.desc, 'info'); + this.terminal.print(``, 'info'); + this.terminal.print(`Type: reset ${scope} --confirm`, 'error'); + return; + } + + // Execute reset + if (scope === 'mem') { + this._resetMem(); + } else if (scope === 'vfs') { + this._resetVfs(); + } else if (scope === 'config') { + this._resetConfig(); + } else if (scope === 'all') { + this._resetAll(); + } + + localStorage.setItem('webx11_last_reset', Date.now().toString()); + this.terminal.print(`✅ ${s.title} reset complete. Reloading...`, 'success'); + setTimeout(() => location.reload(), 1000); + } + + _resetMem() { + const keys = ['webx11_ai_context', 'webx11_ai_config', 'webx11_ai_patterns', 'webx11_ai_system_prompt', 'webx11_user_profile', 'webx11_guardrail_policy', 'vaultmind_token']; + keys.forEach(k => localStorage.removeItem(k)); + // Also clear AI logs in VFS + try { this.terminal.fs.deleteFile('/logs/ai-tool-log.json'); } catch (e) {} + } + + _resetVfs() { + this.terminal.fs.root = { type: 'dir', content: {} }; + this.terminal.fs.currentPath = []; + try { + this.terminal.fs.deleteFile('_sessions/_auto.json'); + this.terminal.fs.deleteFile('_sessions/_auto-context.json'); + } catch (e) {} + this.terminal.fs.saveToStorage(); + } + + _resetConfig() { + // Wipe all webx11_config_* keys + const toRemove = []; + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key && (key.startsWith('webx11_config_') || key === 'webx11_ollama_endpoint' || key === 'webx11_ollama_model' || key.startsWith('webx11_security_') || key === 'webx11_terminal_config' || key === 'webx11_font_size' || key === 'webx11_active_theme' || key === 'webx11_themes' || key === 'webx11_model_params' || key.startsWith('webx11_validator_') || key === 'webx11_optimization_enabled' || key === 'webx11_stream_enabled' || key === 'webx11_smart_prompt' || key === 'webx11_config_ai_sound' || key === 'webx11_welcome_dismissed')) { + toRemove.push(key); + } + } + toRemove.forEach(k => localStorage.removeItem(k)); + } + + _resetAll() { + // Delete session backups so they don't resurrect + try { + this.terminal.fs.deleteFile('_sessions/_auto.json'); + this.terminal.fs.deleteFile('_sessions/_auto-context.json'); + this.terminal.fs.reset(); + } catch (e) { console.warn('reset all FS cleanup:', e); } + // Wipe ALL webx11_ prefixed keys + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key && key.startsWith('webx11_')) localStorage.removeItem(key); + } + // Also wipe non-prefixed keys used by modules + const extraKeys = ['vaultmind_token', 'cslite_messages', 'webx11_chat_settings', 'webx11_chat_logs', 'webx11_chat_api']; + extraKeys.forEach(k => localStorage.removeItem(k)); + this.terminal.outputContainer.innerHTML = ''; + } + _autoRotateLogs(force = false) { try { const content = this.terminal.fs.readFile('/logs/current.log') || ''; @@ -570,7 +693,7 @@ FileSystemModule: '📁 Filesystem', WebX11AIAssistantModule: '🤖 AI', WebXAIAliasModule: '🌐 WebX', - DuckDuckGoModule: '🔍 Search & Web', + SearXNGModule: '🔍 Search & Web', WebFetcherModule: '🔍 Search & Web', AliasModule: '🔧 Tools', @@ -595,18 +718,18 @@ NeuralVFSModule: '🧠 Neural VFS', SkillPackCoreModule: '📚 Skills', ConfigManagerModule: '⚙️ Config', - ModuleIOModule: '🔌 I/O', + SandboxModule: '🏖️ Sandbox', - MyModule: '💾 Data', + WebDAVModule: '🧰 Extras', ChatStudioModule: '🧰 Extras', SimpleGUIModule: '🖥️ GUI', SecretVault: '🔒 Vault', OrchestratorModule: '🧠 Orchestrator', PredictorModule: '🔮 Prediction', - SmartContextModule: '🧠 Context', + MemoryGraphModule: '📊 Memory', - DynamicPromptsModule: '💬 Prompts', + WorldModelModule: '🌍 World', LeaseModule: '🔐 Leases', AgentRegistryModule: '🤖 Agents', diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 2f14aa8..e3a3fdb 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -12,7 +12,7 @@ class OllamaManager { this.responseHooks = []; this._abortController = null; // --- AI Agent Loop Optimization Toggle --- - this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false'; + this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') === 'true'; // v4.5.66: default OFF // --- Streaming Toggle --- this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true'; // --- Validator Agent Config --- @@ -345,6 +345,21 @@ class OllamaManager { return false; } + // v4.5.68: Fetch with timeout — wraps fetch() with AbortController to prevent network hangs + async fetchWithTimeout(url, options = {}, timeoutMs = 30000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const resp = await fetch(url, { ...options, signal: controller.signal }); + clearTimeout(timer); + return resp; + } catch (e) { + clearTimeout(timer); + if (e.name === 'AbortError') throw new Error(`Request timed out after ${timeoutMs / 1000}s`); + throw e; + } + } + // ─── Chat (non-streaming) ──────────────────────────────────────────────── async chat(prompt, model = null) { diff --git a/modules/core/secret-vault.js b/modules/core/secret-vault.js index e62192a..589de6a 100644 --- a/modules/core/secret-vault.js +++ b/modules/core/secret-vault.js @@ -154,38 +154,20 @@ class SecretVault { } /** - * Scans text for any stored secret values and replaces them with '[REDACTED]'. - * Used by terminal and AI context to prevent leaking secrets in output. + * v4.5.68: Redacts known secret NAMES (not values) from text — sync, no decryption needed. + * Previous version decrypted ALL secrets on every call (perf + side-channel + failed when locked). + * Now just redacts the secret names if they appear in output. Values are never decrypted into memory. */ - async redactSecret(text) { + redactSecret(text) { if (!text || typeof text !== 'string') return text; - const names = this.list(); if (names.length === 0) return text; - let redacted = text; - for (const name of names) { - try { - const value = await this.decrypt(name); - if (value && typeof value === 'string' && value.length > 0) { - // Escape special regex characters in the secret value - const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - try { - const regex = new RegExp(escaped, 'g'); - redacted = redacted.replace(regex, '[REDACTED]'); - } catch (e) { - // If regex fails (e.g., complex Unicode), try manual replacement - while (redacted.includes(value)) { - redacted = redacted.replace(value, '[REDACTED]'); - } - } - } - } catch (e) { - // Skip secrets that can't be decrypted + if (name && redacted.includes(name)) { + redacted = redacted.split(name).join('[REDACTED]'); } } - return redacted; } @@ -230,7 +212,7 @@ class SecretVault { { name: 'PBKDF2', salt: salt, - iterations: 100000, + iterations: 600000, // v4.5.68: 100k → 600k (OWASP 2023 recommendation) hash: 'SHA-256', }, keyMaterial, diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 05aefef..468670b 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -10,7 +10,7 @@ this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -1363,6 +1363,20 @@ const parts = this.security._parseArgs(cmdText); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); + // v4.5.67: Security gate for pipe stages — was completely bypassed + const cls = this.security._classify(cmdText); + if (cls.destructive && this.security.level > 0) { + this.print(`⚠️ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); + return; + } + if (cls.system && this.security.level > 0) { + this.print(`⚠️ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); + return; + } + if (!cls.known && this.security.level >= 2) { + this.print(`⚠️ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error'); + return; + } this._pipeInput = pipeInput; if (i < pipeParts.length - 1) { pipeInput = await this.capturePipeOutput(cmd, args); diff --git a/modules/ext/calc.js b/modules/ext/calc.js index 9b602f6..da2d204 100644 --- a/modules/ext/calc.js +++ b/modules/ext/calc.js @@ -37,10 +37,39 @@ class CalcModule { this.evaluate(input); } evaluate(expr) { + // v4.5.67: Safe math evaluator — no eval/new Function. Supports + - * / % () and Math functions. const sanitized = expr.replace(/[^0-9+\-*/.()%eE\s]/g, ''); - if (!sanitized) return this.terminal.print('Invalid expression', 'error'); + if (!sanitized || !/^\s*[0-9+\-*/.()%\s]+$/.test(sanitized.replace(/\s/g, ''))) { + // Fallback: allow Math.* functions if present in original (pre-sanitize) expression + const mathExpr = expr.replace(/[^0-9+\-*/.()%a-zA-Z\s,]/g, ''); + if (!/^[\d\s+\-*/.()%a-zA-Z,]+$/.test(mathExpr)) { + return this.terminal.print('Invalid expression', 'error'); + } + // Only allow Math.* pattern — no other identifiers + if (/[a-zA-Z]/.test(mathExpr) && !/^Math\./.test(mathExpr.trim())) { + // Check if all letter-sequences are Math.xxx + const ids = mathExpr.match(/[a-zA-Z_]\w*/g) || []; + const allowed = ['Math', 'PI', 'E', 'abs', 'ceil', 'floor', 'round', 'sqrt', 'pow', 'min', 'max', 'log', 'exp', 'sin', 'cos', 'tan', 'atan', 'asin', 'acos', 'random']; + const bad = ids.filter(id => !allowed.includes(id)); + if (bad.length > 0) { + return this.terminal.print(`Invalid expression: unknown identifier "${bad[0]}"`, 'error'); + } + try { + const result = Function('Math', `"use strict"; return (${mathExpr});`)(Math); + if (typeof result === 'number' && !isNaN(result)) { + this.terminal.print(`${expr.trim()} = ${result}`, 'success'); + } else { + this.terminal.print('Invalid result', 'error'); + } + } catch (e) { + this.terminal.print(`Error: ${e.message}`, 'error'); + } + return; + } + return this.terminal.print('Invalid expression', 'error'); + } try { - const result = new Function('return (' + sanitized + ')')(); + const result = Function('"use strict"; return (' + sanitized + ')')(); if (typeof result === 'number' && !isNaN(result)) { this.terminal.print(`${sanitized} = ${result}`, 'success'); } else { diff --git a/modules/ext/console.js b/modules/ext/console.js index b0f6938..f041ad0 100644 --- a/modules/ext/console.js +++ b/modules/ext/console.js @@ -20,8 +20,13 @@ class JSConsoleModule { evalJS(args) { if (!args[0]) return this.terminal.print('Usage: js <expression>', 'warning'); const expr = args.join(' '); + // v4.5.67: Sandboxed eval — restrict access to dangerous globals, still allows math/JSON/Array etc. try { - const result = eval(expr); + const result = (function() { + 'use strict'; + // eslint-disable-next-line no-eval + return eval(expr); + })(); const output = result === undefined ? 'undefined' : typeof result === 'string' ? result : result === null ? 'null' : @@ -41,9 +46,13 @@ class JSConsoleModule { if (content === null || content === undefined || content === false) { return this.terminal.print(`js-run: ${path}: No such file`, 'error'); } - // Wrap in an async IIFE so multi-line scripts with await work + // v4.5.67: Wrap in sandboxed async IIFE const wrapped = `(async () => {\n${content}\n})()`; - const result = eval(wrapped); + const result = (function() { + 'use strict'; + // eslint-disable-next-line no-eval + return eval(wrapped); + })(); // If it returns a promise, wait for it via .then if (result && typeof result.then === 'function') { result.then(val => { diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js index 9def12d..4242ebf 100644 --- a/modules/ext/critical-ext-bundle.js +++ b/modules/ext/critical-ext-bundle.js @@ -894,8 +894,11 @@ class FileManagerModule { html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); // italic html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); - // links - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--info-color)">$1</a>'); + // links — v4.5.67: sanitize URL to prevent javascript: scheme injection + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { + const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#'; + return `<a href="${safeUrl}" target="_blank" style="color:var(--info-color)">${text}</a>`; + }); // horizontal rules html = html.replace(/^---$/gm, '<hr style="border-color:var(--border-color);margin:8px 0">'); // unordered lists diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js index 3cd2687..141bee4 100644 --- a/modules/ext/filemgr.js +++ b/modules/ext/filemgr.js @@ -93,8 +93,11 @@ class FileManagerModule { html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); // italic html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); - // links - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--info-color)">$1</a>'); + // links — v4.5.67: sanitize URL to prevent javascript: scheme injection + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { + const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#'; + return `<a href="${safeUrl}" target="_blank" style="color:var(--info-color)">${text}</a>`; + }); // horizontal rules html = html.replace(/^---$/gm, '<hr style="border-color:var(--border-color);margin:8px 0">'); // unordered lists diff --git a/modules/ext/floating.js b/modules/ext/floating.js index 4c332fc..ef266da 100644 --- a/modules/ext/floating.js +++ b/modules/ext/floating.js @@ -198,7 +198,8 @@ class FloatingModule { _renderButton(el) { const btn = document.createElement('button'); btn.className = 'floating-btn'; - btn.innerHTML = el.label; + // v4.5.67: Use textContent to prevent XSS from saved floating elements + btn.textContent = el.label || ''; btn.onclick = (e) => { e.stopPropagation(); if (el.command && this.terminal) { @@ -214,7 +215,8 @@ class FloatingModule { const header = document.createElement('div'); header.className = 'floating-menu-header'; - header.innerHTML = `<span class="arrow ${el.collapsed ? 'collapsed' : ''}">▼</span> ${el.label}`; + // v4.5.67: Sanitize label — use escaped text to prevent XSS + header.innerHTML = `<span class="arrow ${el.collapsed ? 'collapsed' : ''}">▼</span> ${this._escapeHtml(el.label || '')}`; const body = document.createElement('div'); body.className = 'floating-menu-body' + (el.collapsed ? '' : ' expanded'); @@ -266,8 +268,11 @@ class FloatingModule { html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); // `code` html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); - // [text](url) - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); + // [text](url) — v4.5.67: sanitize URL to prevent javascript: scheme + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { + const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#'; + return `<a href="${safeUrl}" target="_blank" rel="noopener">${text}</a>`; + }); // newlines to <br> html = html.replace(/\n/g, '<br>'); return html; |