diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 59 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 5 | ||||
| -rw-r--r-- | modules/core/terminal.js | 54 |
3 files changed, 90 insertions, 28 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index cfdfe11..29c6cc6 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -2611,23 +2611,27 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } this.blockedCount++; const sug = this.suggest(cmd); - if (sug) { - this.terminal.print(`Security: Unknown command "${cmd}". Did you mean: "${sug.cmd}"?`, 'warning'); - } else { - this.terminal.print(`Security: Unknown command "${cmd}" — blocked. Use 'security-level 0' to disable.`, 'warning'); - } - // At level 1: suggest/fix but still route to AI (terminal is also a chat interface) // At level 2+: block entirely (paranoid) if (this.level >= 2) { - this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).`, 'error'); + this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).${sug ? ` Did you mean: "${sug.cmd}"?` : ''}`, 'error'); return; } - if (this.level >= 1) { - if (sug) { - this.terminal.print(`Security: Did you mean "${sug.cmd}"? Routing to AI...`, 'warning'); - } else { - this.terminal.print(`Security: Routing to AI...`, 'info'); - } + // Level 1: typo of a safe whitelisted command → one-tap clickable fix, no AI call burned + if (sug && sug.dist <= 1 && this.whitelist.has(sug.cmd) && this.terminal.showSuggestionBar && !this._aiToolCall) { + this.terminal.print(`Unknown command "${cmd}" — did you mean:`, 'warning'); + this.terminal.showSuggestionBar(sug.cmd, args); + return; + } + // AI-invoked commands must not route back to AI (infinite loop) — return structured error + if (this._aiToolCall) { + this.terminal.print(`Command not found: ${cmd}${sug ? `. Did you mean: ${sug.cmd}?` : ''}`, 'error'); + return; + } + // Level 1 fallback: route to AI (terminal is also a chat interface) + if (sug) { + this.terminal.print(`Security: Unknown command "${cmd}" — routing to AI (did you mean "${sug.cmd}"?)...`, 'warning'); + } else { + this.terminal.print(`Security: Unknown command "${cmd}" — routing to AI...`, 'info'); } return this._tryAIRoute(rawInput); } @@ -3177,6 +3181,30 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this.outputContainer.appendChild(line); this.scrollToBottom(); } + showSuggestionBar(cmd, args) { + // One-tap typo fix: renders clickable "✅ Run <sug> <args> / ❌ Dismiss" bar + const area = document.getElementById('ai-confirm-area'); + const runCmd = cmd + ((args && args.length) ? ' ' + args.join(' ') : ''); + if (!area) { + // Fallback for hosts without the confirm UI: print clickable command + this.print(`Did you mean: ${runCmd} — type it to run.`, 'info'); + return; + } + area.innerHTML = ` + <span class="confirm-desc">💡 Did you mean: <code>${runCmd}</code>?</span> + <button class="confirm-btn" id="cmd-confirm-allow" onclick="(function(){var a=document.getElementById('cmd-confirm-allow');if(a.__allow) a.__allow();}).call(this)">✅ Run it</button> + <button class="confirm-btn deny" id="cmd-confirm-deny" onclick="(function(){var d=document.getElementById('cmd-confirm-deny');if(d.__deny) d.__deny();}).call(this)">❌ Dismiss</button> + `; + area.classList.add('visible'); + const allowBtn = document.getElementById('cmd-confirm-allow'); + const denyBtn = document.getElementById('cmd-confirm-deny'); + const onAllow = () => { area.classList.remove('visible'); area.innerHTML = ''; this.executeCommand(runCmd); }; + const onDeny = () => { area.classList.remove('visible'); area.innerHTML = ''; this.print('Dismissed.', 'text-muted'); }; + allowBtn.addEventListener('click', onAllow); + denyBtn.addEventListener('click', onDeny); + allowBtn.__allow = onAllow; + denyBtn.__deny = onDeny; + } showConfirmationBar(desc) { const area = document.getElementById('ai-confirm-area'); if (!area) { @@ -10221,7 +10249,10 @@ buildSystemPrompt() { const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; - try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.print = originalPrint; } + // Flag security so unknown commands return an error instead of routing back to AI (loop guard) + const prevAiFlag = this.terminal.security._aiToolCall; + this.terminal.security._aiToolCall = true; + try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.security._aiToolCall = prevAiFlag; this.terminal.print = originalPrint; } // 📸 Diff and append shadow report const shadowDiff = this._diffShadowVFS(); if (shadowDiff && shadowDiff.length > 0) { diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 73078d0..e134d69 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -2259,7 +2259,10 @@ buildSystemPrompt() { const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; this.terminal.print = (text, type) => { output += text + '\n'; }; - try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.print = originalPrint; } + // Flag security so unknown commands return an error instead of routing back to AI (loop guard) + const prevAiFlag = this.terminal.security._aiToolCall; + this.terminal.security._aiToolCall = true; + try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.security._aiToolCall = prevAiFlag; this.terminal.print = originalPrint; } // 📸 Diff and append shadow report const shadowDiff = this._diffShadowVFS(); if (shadowDiff && shadowDiff.length > 0) { diff --git a/modules/core/terminal.js b/modules/core/terminal.js index f322446..b058346 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -185,23 +185,27 @@ } this.blockedCount++; const sug = this.suggest(cmd); - if (sug) { - this.terminal.print(`Security: Unknown command "${cmd}". Did you mean: "${sug.cmd}"?`, 'warning'); - } else { - this.terminal.print(`Security: Unknown command "${cmd}" — blocked. Use 'security-level 0' to disable.`, 'warning'); - } - // At level 1: suggest/fix but still route to AI (terminal is also a chat interface) // At level 2+: block entirely (paranoid) if (this.level >= 2) { - this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).`, 'error'); + this.terminal.print(`Security: Unknown command "${cmd}" blocked (level ${this.level}).${sug ? ` Did you mean: "${sug.cmd}"?` : ''}`, 'error'); return; } - if (this.level >= 1) { - if (sug) { - this.terminal.print(`Security: Did you mean "${sug.cmd}"? Routing to AI...`, 'warning'); - } else { - this.terminal.print(`Security: Routing to AI...`, 'info'); - } + // Level 1: typo of a safe whitelisted command → one-tap clickable fix, no AI call burned + if (sug && sug.dist <= 1 && this.whitelist.has(sug.cmd) && this.terminal.showSuggestionBar && !this._aiToolCall) { + this.terminal.print(`Unknown command "${cmd}" — did you mean:`, 'warning'); + this.terminal.showSuggestionBar(sug.cmd, args); + return; + } + // AI-invoked commands must not route back to AI (infinite loop) — return structured error + if (this._aiToolCall) { + this.terminal.print(`Command not found: ${cmd}${sug ? `. Did you mean: ${sug.cmd}?` : ''}`, 'error'); + return; + } + // Level 1 fallback: route to AI (terminal is also a chat interface) + if (sug) { + this.terminal.print(`Security: Unknown command "${cmd}" — routing to AI (did you mean "${sug.cmd}"?)...`, 'warning'); + } else { + this.terminal.print(`Security: Unknown command "${cmd}" — routing to AI...`, 'info'); } return this._tryAIRoute(rawInput); } @@ -751,6 +755,30 @@ this.outputContainer.appendChild(line); this.scrollToBottom(); } + showSuggestionBar(cmd, args) { + // One-tap typo fix: renders clickable "✅ Run <sug> <args> / ❌ Dismiss" bar + const area = document.getElementById('ai-confirm-area'); + const runCmd = cmd + ((args && args.length) ? ' ' + args.join(' ') : ''); + if (!area) { + // Fallback for hosts without the confirm UI: print clickable command + this.print(`Did you mean: ${runCmd} — type it to run.`, 'info'); + return; + } + area.innerHTML = ` + <span class="confirm-desc">💡 Did you mean: <code>${runCmd}</code>?</span> + <button class="confirm-btn" id="cmd-confirm-allow" onclick="(function(){var a=document.getElementById('cmd-confirm-allow');if(a.__allow) a.__allow();}).call(this)">✅ Run it</button> + <button class="confirm-btn deny" id="cmd-confirm-deny" onclick="(function(){var d=document.getElementById('cmd-confirm-deny');if(d.__deny) d.__deny();}).call(this)">❌ Dismiss</button> + `; + area.classList.add('visible'); + const allowBtn = document.getElementById('cmd-confirm-allow'); + const denyBtn = document.getElementById('cmd-confirm-deny'); + const onAllow = () => { area.classList.remove('visible'); area.innerHTML = ''; this.executeCommand(runCmd); }; + const onDeny = () => { area.classList.remove('visible'); area.innerHTML = ''; this.print('Dismissed.', 'text-muted'); }; + allowBtn.addEventListener('click', onAllow); + denyBtn.addEventListener('click', onDeny); + allowBtn.__allow = onAllow; + denyBtn.__deny = onDeny; + } showConfirmationBar(desc) { const area = document.getElementById('ai-confirm-area'); if (!area) { |