diff options
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 72 |
1 files changed, 59 insertions, 13 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 5a4341a..ed76a8a 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -49,9 +49,20 @@ const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const optimizationActive = this.terminal?.ollamaManager?.optimizationEnabled !== false; + // Neural VFS staleness check + let nvfsNote = ''; + try { + const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; + if (nvfs && nvfs.lastScan && nvfs.lastScan !== '(loaded from disk)') { + const ageMs = Date.now() - new Date(nvfs.lastScan).getTime(); + if (ageMs > 14400000) { // > 4 hours + nvfsNote = `\nNote: Neural VFS index is ${Math.floor(ageMs/3600000)}h old. nvfs-resolve results may be stale. Use list_files/read_file directly for live data.`; + } + } + } catch (e) {} const CR = String.fromCharCode(10); const optNote = CR + CR + '== TOOL EXECUTION RULES (STRICT) ==' + CR + 'STRICT RULE 1: NEVER describe what the filesystem looks like from your training data. ALWAYS call list_files() or read_file() first, then report the ACTUAL result.' + CR + 'STRICT RULE 2: If you return text without calling any tool, you have failed. Use tools for every factual question.' + CR + 'STRICT RULE 3: A response like "/bin/, /etc/, /home/" without calling list_files is ILLEGAL. Call the tool.' + CR + 'STRICT RULE 4: When calling a tool, base your answer ENTIRELY on the tool result. If result is empty, say "empty".' + CR + CR; - return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE == + return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n${nvfsNote}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.${optNote}== TOOL REFERENCE == You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE): FILESYSTEM: @@ -235,7 +246,7 @@ RULES: type: 'function', function: { name: 'run_command', - description: 'Run terminal command', + description: 'Run terminal command (safe-read commands auto-approved; mutating ones require confirmation)', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Command to run' } }, required: ['command'] } } }, { @@ -506,7 +517,7 @@ RULES: 'ai-ignore': 'ai-ignore — Suppress validator warnings for this session', 'ai-stop': 'ai-stop — Cancel current AI operation', 'ai-model': 'ai-model [model] — Show or set AI model', - 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context)' + 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))' }; } registerCommands() { @@ -776,8 +787,8 @@ RULES: 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; - const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name)); - const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name)); + const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments)); + const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name, tc.function.arguments)); // Execute non-destructive tools in parallel (independent read-only operations) const safeResults = await Promise.all(safeTools.map(async (tc) => { const fn = tc.function; @@ -969,10 +980,18 @@ RULES: } } async confirmActionMulti(toolCalls) { + // No-confirm mode: auto-approve + if (this._confirmTimeoutMs === 0) { + this.terminal.print(`⚡ No-confirm mode: auto-approving ${toolCalls.map(tc => tc.function.name).join(', ')}`, 'info'); + return true; + } const names = toolCalls.map(tc => tc.function.name).join(', '); const args = toolCalls.map(tc => typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments)).join(' | '); const truncatedArgs = args.length > 60 ? args.substring(0, 60) + '...' : args; - this.terminal.printHTML(`⚠️ AI wants to: ${names}(${truncatedArgs}). <span class="cmd-link" onclick="webx11.executeCommand('ai-allow')" style="color:var(--success-color);cursor:pointer;text-decoration:underline;">ai-allow</span> to confirm, <span class="cmd-link" onclick="webx11.executeCommand('ai-deny')" style="color:var(--error-color);cursor:pointer;text-decoration:underline;">ai-deny</span> to refuse. (Auto-denies in 120s)`); + const timeoutLabel = this._confirmTimeoutMs === 0 + ? '' + : ` (Auto-denies in ${this._confirmTimeoutMs / 1000}s)`; + this.terminal.printHTML(`⚠️ AI wants to: ${names}(${truncatedArgs}). <span class="cmd-link" onclick="webx11.executeCommand('ai-allow')" style="color:var(--success-color);cursor:pointer;text-decoration:underline;">ai-allow</span> to confirm, <span class="cmd-link" onclick="webx11.executeCommand('ai-deny')" style="color:var(--error-color);cursor:pointer;text-decoration:underline;">ai-deny</span> to refuse.${timeoutLabel}`); // Show confirm bar OUTSIDE scrollable output (always visible) const area = document.getElementById('ai-confirm-area'); if (!area) { @@ -1390,8 +1409,19 @@ RULES: } } // 🔒 OVERRIDES for isDestructive — adds runTool intercept - isDestructive(name) { - return name === 'write_file' || name === 'delete_file' || name === 'run_command' || name === 'make_directory' || name === 'edit_module'; + // Now command-aware: run_command is only destructive for mutating commands + isDestructive(name, args = {}) { + if (name === 'run_command') { + const cmdStr = typeof args === 'object' ? (args?.command || '') : String(args); + const cmd = cmdStr.split(' ')[0].toLowerCase(); + const safeRead = new Set(['ls','cat','pwd','echo','date','env','whoami','which','hostname', + 'cal','tree','find','grep','head','tail','sort','wc','du','diff','basename','dirname', + 'yes','seq','cut','uniq','expand','printf','base64','help','clear','set','export', + 'unalias','history','ll','h','mem','status','df','free','top','ps','health-check', + 'system-health','modules','listmods','modules-list','get-help-info','alias']); + return !safeRead.has(cmd); + } + return ['write_file','delete_file','make_directory','edit_module'].includes(name); } async runTool(name, argsStr) { let a = {}; @@ -1420,10 +1450,26 @@ RULES: const parts = p.split('/'); const dir = this.terminal.fs.root; let cur = dir; + let found = true; for (const part of parts) { if (part === '..') { saved.pop(); continue; } if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; - else return JSON.stringify({ error: `Directory not found: ${p}` }); + else { found = false; break; } + } + if (!found) { + // Retry from root — cwd may be stale + this.terminal.fs.currentPath = []; + cur = this.terminal.fs.root; + found = true; + for (const part of parts) { + if (part === '..') continue; + if (part && cur.content[part] && cur.content[part].type === 'dir') cur = cur.content[part]; + else { found = false; break; } + } + if (!found) { + this.terminal.fs.currentPath = saved; + return JSON.stringify({ error: `Directory not found: ${p}` }); + } } this.terminal.fs.currentPath = saved.concat(parts.filter(x => x !== '..')); } @@ -1541,7 +1587,7 @@ RULES: const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; - try { await this.terminal.dispatchCommand(cmd.split(' ')[0].toLowerCase(), cmd.split(' ').slice(1)); } finally { this.terminal.print = originalPrint; } + try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.print = originalPrint; } // 📸 Diff and append shadow report const shadowDiff = this._diffShadowVFS(); if (shadowDiff && shadowDiff.length > 0) { @@ -2104,11 +2150,11 @@ RULES: 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(`confirm-timeout: ${this._confirmTimeoutMs / 1000}s (default: 120s)`, ''); + 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)`, ''); this.terminal.print('Usage: ai-config <key> <value>', 'info'); - this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); + this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', 'info'); return; } const key = args[0]; @@ -2127,7 +2173,7 @@ RULES: this.maxContextTokens = n; } else if (key === 'confirm-timeout') { const n = parseInt(val); - if (isNaN(n) || n < 5 || n > 600) return this.terminal.print('confirm-timeout must be 5-600 seconds', 'warning'); + if (isNaN(n) || n < 0) return this.terminal.print('confirm-timeout must be 0+ seconds (0 = no confirm / auto-approve all)', 'warning'); this._confirmTimeoutMs = n * 1000; } else if (key === 'chat-lock-timeout') { const n = parseInt(val); |