aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js727
-rw-r--r--modules/core/module-aiassist.js31
-rw-r--r--modules/core/module-chatstudio.js5
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-manager.js15
-rw-r--r--modules/core/module-servermon.js5
-rw-r--r--modules/core/module-simplegui.js13
-rw-r--r--modules/core/module-system.js139
-rw-r--r--modules/core/ollama-manager.js17
-rw-r--r--modules/core/secret-vault.js32
-rw-r--r--modules/core/terminal.js16
11 files changed, 455 insertions, 547 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
+ }
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\\"/g, '&quot;').replace(/'/g, '&#39;');
};
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;');
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;');
}
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\\"/g, '&quot;').replace(/'/g, '&#39;');
};
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
- .replace(/"/g, '&quot;');
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;');
}
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
+ }
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);