diff options
| author | Agando <roduccomm@gmail.com> | 2026-09-29 15:56:27 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-09-29 15:56:27 +0200 |
| commit | dde918a235ff8eac41f6d205e954dc4a4daa5b01 (patch) | |
| tree | a58da7664e938d6d4d0fca0974932139e8b8584c /modules/core/core-bundle.js | |
| parent | 0e454aecf55d862b7618ebd1c98ca9fd71a15817 (diff) | |
newOS4 v4.6.15: reentrant print-capture stack, AI tail -f block, read-only sed in safeRead, security test harness (21 checks)
Diffstat (limited to 'modules/core/core-bundle.js')
| -rw-r--r-- | modules/core/core-bundle.js | 62 |
1 files changed, 42 insertions, 20 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index c7a91dc..6127f36 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -2726,6 +2726,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this._pipeInput = null; this._pipeBufferSize = 5000; this._activeFollow = null; + // Print-capture stack: [{output: ''}] — see print() head. LIFO order: + // nested/parallel captures each get their own buffer. + this._printCaptureStack = []; this._historyBuffer = []; this._historyMaxLines = 50; this._historySavePending = false; @@ -2783,6 +2786,15 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm _invalidateCommandCache() { if (this.security) this.security.invalidateCommandCache(); } + _pushPrintCapture(onChunk) { + // onChunk(buffer) called at pop with the full captured text + if (!this._printCaptureStack) this._printCaptureStack = []; + this._printCaptureStack.push({ output: '', onChunk }); + } + _popPrintCapture() { + const cap = this._printCaptureStack && this._printCaptureStack.pop(); + if (cap && cap.onChunk) cap.onChunk(cap.output); + } stopFollow(msg) { if (!this._activeFollow) return false; clearInterval(this._activeFollow.interval); @@ -2968,6 +2980,14 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm label.textContent = prefix + (path === '/' ? '$' : path + ' $'); } print(text, type = '') { + // Print-capture stack: while active, output goes to the innermost capture + // buffer instead of the DOM. Reentrant — parallel/nested captures each + // collect their own output (race-safe, replaces monkey-patched print). + if (this._printCaptureStack && this._printCaptureStack.length > 0) { + const cap = this._printCaptureStack[this._printCaptureStack.length - 1]; + cap.output += text + '\n'; + return; + } if (['error','warning','success','info','text-muted'].includes(type) && !this._showSystemMessages && !type.startsWith('command')) { // Still log to history buffer so it can be shown again when toggled on, // but don't create DOM nodes while system messages are hidden. @@ -3958,13 +3978,12 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this._pipeInput = null; } async capturePipeOutput(cmd, args) { - const originalPrint = this.print.bind(this); let output = ''; - this.print = (text, type) => { output += text + '\n'; }; + this._pushPrintCapture(buf => { output += buf; }); try { await this.dispatchCommand(cmd, args); } finally { - this.print = originalPrint; + this._popPrintCapture(); } const trimmed = output.trim(); if (trimmed.length > this._pipeBufferSize) { @@ -6432,6 +6451,10 @@ class FileSystemModule { if (c === null || c === undefined) return this.terminal.print('tail: ' + file + ': No such file', 'error'); this.terminal.print(c.split('\n').slice(-n).join('\n')); if (!follow) return; + // AI tool calls cannot press Ctrl+C — auto-downgrade to a bounded read instead of a live timer + if (this.terminal.security && this.terminal.security._aiToolDepth > 0) { + return this.terminal.print('tail: follow mode (-f) is interactive-only; showing last lines instead (use tail -n ' + Math.max(n, 50) + ' for more)', 'warning'); + } // stop any previous follow before starting a new one if (this.terminal._activeFollow && this.terminal._activeFollow.interval) clearInterval(this.terminal._activeFollow.interval); this.terminal._activeFollow = null; @@ -8937,24 +8960,22 @@ buildSystemPrompt() { } // ─── Self-Improvement Helpers ─── async _captureCommandOutput(cmd) { - 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++; + this.terminal._pushPrintCapture(buf => { output += buf; }); try { await this.terminal.security.checkAndRun(cmd); } - finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; } + finally { this.terminal._popPrintCapture(); this.terminal.security._aiToolDepth--; } return output.trim(); } async _captureShellCommand(cmd) { // Routes through executeCommand (shell parser) for >, |, &&, export, etc. - 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++; + this.terminal._pushPrintCapture(buf => { output += buf; }); try { await this.terminal.executeCommand(cmd); } - finally { this.terminal.security._aiToolDepth--; this.terminal.print = originalPrint; } + finally { this.terminal._popPrintCapture(); this.terminal.security._aiToolDepth--; } return output.trim(); } async _captureConsoleErrors(durationMs) { @@ -10003,11 +10024,10 @@ buildSystemPrompt() { this._snapshotShadowVFS(); const cmdName = cmd.split(' ')[0].toLowerCase(); const cmdArgs = cmd.split(' ').slice(1); - const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; - this.terminal.print = (text, type) => { output += text + '\n'; }; + this.terminal._pushPrintCapture(buf => { output += buf; }); this.terminal.dispatchCommand(cmdName, cmdArgs).then(() => { - this.terminal.print = originalPrint; + this.terminal._popPrintCapture(); const diff = this._diffShadowVFS(false); if (!diff || diff.length === 0) { this.terminal.print('📦 Shadow: No filesystem changes detected.', 'success'); @@ -10024,7 +10044,7 @@ buildSystemPrompt() { this.terminal.print('🔒 VFS modified — type "shadow-commit" to accept or "shadow-rollback" to undo.', 'warning'); } }).catch(e => { - this.terminal.print = originalPrint; + this.terminal._popPrintCapture(); this.terminal.print('⚠️ Shadow command error: ' + e.message, 'error'); }); } @@ -10052,8 +10072,11 @@ buildSystemPrompt() { if (name === 'run_command') { const cmdStr = typeof args === 'object' ? (args?.command || '') : String(args); const cmd = cmdStr.split(' ')[0].toLowerCase(); + // sed: read-only UNLESS -i (in-place write) or it starts a live tail -f timer + if (cmd === 'sed' && /(^|\s)-i(\s|$)/.test(cmdStr)) return true; + if (cmd === 'tail' && /(^|\s)-f\b|(^|\s)--follow\b/.test(cmdStr)) return false; // tail -f self-downgrades for AI (module-fs guard) 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', + 'cal','tree','find','grep','head','tail','sed','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', @@ -10239,10 +10262,9 @@ buildSystemPrompt() { if (!kw) return JSON.stringify({ error: 'No keyword' }); const mod = this.terminal.moduleManager.modules['WpmModule']; if (mod) { - const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; - this.terminal.print = (text, type) => { output += text + '\n'; }; - try { await mod.search(kw); } finally { this.terminal.print = originalPrint; } + this.terminal._pushPrintCapture(buf => { output += buf; }); + try { await mod.search(kw); } finally { this.terminal._popPrintCapture(); } return output.trim() || 'Search completed'; } return JSON.stringify({ error: 'WPM module not available' }); @@ -10252,13 +10274,13 @@ buildSystemPrompt() { if (!cmd) return JSON.stringify({ error: 'No command provided' }); // 📸 Shadow VFS auto-snapshot before run_command this._snapshotShadowVFS(); - const originalPrint = this.terminal.print.bind(this.terminal); let output = ''; - this.terminal.print = (text, type) => { output += text + '\n'; }; // 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; } + this.terminal._pushPrintCapture(buf => { output += buf; }); + try { await this.terminal.security.checkAndRun(cmd); } + finally { this.terminal._popPrintCapture(); this.terminal.security._aiToolDepth--; } // Parse per-call captured output (race-free) — not shared security state const nfMatch = output.match(/Command not found: ([^\s.]+)(?:\. Did you mean: ([^\s.?]+)\?)?/); if (nfMatch) { |