aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-aiassist.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-07 18:00:19 +0200
committerAgando <roduccomm@gmail.com>2026-06-07 18:00:19 +0200
commit0fa227601d51263b0eb787d53225de3f0b44cad1 (patch)
tree2d9f440af92f5b3ead1bea66bd2270a6eb270131 /modules/core/module-aiassist.js
parentab235396568a7e61f02eed436e2400975c752d01 (diff)
auto: 2026-06-07_18:00 changes
Diffstat (limited to 'modules/core/module-aiassist.js')
-rw-r--r--modules/core/module-aiassist.js72
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);