aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js30
-rw-r--r--modules/core/module-aiassist.js21
-rw-r--r--modules/core/terminal.js9
4 files changed, 47 insertions, 19 deletions
diff --git a/main.html b/main.html
index e379ef7..73a64f1 100644
--- a/main.html
+++ b/main.html
@@ -1345,13 +1345,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.13"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.14"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.13"></script>
+<script src="modules/core/module-loader.js?v=v4.6.14"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.13"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.14"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 29c6cc6..942f836 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -2441,6 +2441,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this.systemCmds = new Set(this._baseSystem);
this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
this.whitelist = new Set(this._baseWhitelist);
+ // AI loop-guard: depth counter (reentrant, race-safe vs parallel tool calls)
+ this._aiToolDepth = 0;
+ this._lastUnknownCmd = null;
// Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
this._loadSecurityDeltas();
this._registerBuiltIn();
@@ -2594,6 +2597,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return { cmd, args, known, destructive, system, unknown, hasSuspicious };
}
async checkAndRun(rawInput) {
+ this._lastUnknownCmd = null;
const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput);
// Level 3 Audit: just log and allow
if (this.level === 3) {
@@ -2617,13 +2621,14 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
// 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) {
+ if (sug && sug.dist <= 1 && this.whitelist.has(sug.cmd) && this.terminal.showSuggestionBar && !(this._aiToolDepth > 0)) {
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) {
+ if (this._aiToolDepth > 0) {
+ this._lastUnknownCmd = cmd;
this.terminal.print(`Command not found: ${cmd}${sug ? `. Did you mean: ${sug.cmd}?` : ''}`, 'error');
return;
}
@@ -8938,8 +8943,10 @@ buildSystemPrompt() {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
+ // Loop guard: AI-initiated commands must not route back to AI on unknown command
+ this.terminal.security._aiToolDepth++;
try { await this.terminal.security.checkAndRun(cmd); }
- finally { this.terminal.print = originalPrint; }
+ finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; }
return output.trim();
}
async _captureShellCommand(cmd) {
@@ -8947,8 +8954,10 @@ buildSystemPrompt() {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
+ // Loop guard: AI-initiated commands must not route back to AI on unknown command
+ this.terminal.security._aiToolDepth++;
try { await this.terminal.executeCommand(cmd); }
- finally { this.terminal.print = originalPrint; }
+ finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; }
return output.trim();
}
async _captureConsoleErrors(durationMs) {
@@ -10249,10 +10258,15 @@ buildSystemPrompt() {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
- // 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; }
+ // Loop guard (depth counter — race-safe under Promise.all tool fan-out):
+ // unknown commands return an error instead of routing back to AI
+ this.terminal.security._aiToolDepth++;
+ try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; }
+ const unknownCmd = this.terminal.security._lastUnknownCmd;
+ if (unknownCmd) {
+ const sug = this.terminal.security.suggest(unknownCmd);
+ return JSON.stringify({ error: `Command not found: ${unknownCmd}`, suggestion: sug ? sug.cmd : null });
+ }
// 📸 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 e134d69..85aae46 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -948,8 +948,10 @@ buildSystemPrompt() {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
+ // Loop guard: AI-initiated commands must not route back to AI on unknown command
+ this.terminal.security._aiToolDepth++;
try { await this.terminal.security.checkAndRun(cmd); }
- finally { this.terminal.print = originalPrint; }
+ finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; }
return output.trim();
}
async _captureShellCommand(cmd) {
@@ -957,8 +959,10 @@ buildSystemPrompt() {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
+ // Loop guard: AI-initiated commands must not route back to AI on unknown command
+ this.terminal.security._aiToolDepth++;
try { await this.terminal.executeCommand(cmd); }
- finally { this.terminal.print = originalPrint; }
+ finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; }
return output.trim();
}
async _captureConsoleErrors(durationMs) {
@@ -2259,10 +2263,15 @@ buildSystemPrompt() {
const originalPrint = this.terminal.print.bind(this.terminal);
let output = '';
this.terminal.print = (text, type) => { output += text + '\n'; };
- // 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; }
+ // Loop guard (depth counter — race-safe under Promise.all tool fan-out):
+ // unknown commands return an error instead of routing back to AI
+ this.terminal.security._aiToolDepth++;
+ try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; }
+ const unknownCmd = this.terminal.security._lastUnknownCmd;
+ if (unknownCmd) {
+ const sug = this.terminal.security.suggest(unknownCmd);
+ return JSON.stringify({ error: `Command not found: ${unknownCmd}`, suggestion: sug ? sug.cmd : null });
+ }
// 📸 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 b058346..5c7a8fb 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -15,6 +15,9 @@
this.systemCmds = new Set(this._baseSystem);
this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
this.whitelist = new Set(this._baseWhitelist);
+ // AI loop-guard: depth counter (reentrant, race-safe vs parallel tool calls)
+ this._aiToolDepth = 0;
+ this._lastUnknownCmd = null;
// Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
this._loadSecurityDeltas();
this._registerBuiltIn();
@@ -168,6 +171,7 @@
return { cmd, args, known, destructive, system, unknown, hasSuspicious };
}
async checkAndRun(rawInput) {
+ this._lastUnknownCmd = null;
const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput);
// Level 3 Audit: just log and allow
if (this.level === 3) {
@@ -191,13 +195,14 @@
return;
}
// 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) {
+ if (sug && sug.dist <= 1 && this.whitelist.has(sug.cmd) && this.terminal.showSuggestionBar && !(this._aiToolDepth > 0)) {
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) {
+ if (this._aiToolDepth > 0) {
+ this._lastUnknownCmd = cmd;
this.terminal.print(`Command not found: ${cmd}${sug ? `. Did you mean: ${sug.cmd}?` : ''}`, 'error');
return;
}