mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-07-20 23:57:11 -04:00
[PR #246] [MERGED] UI terminal: modular architecture, Unicode fix, and security hardening #265
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/vxcontrol/pentagi/pull/246
Author: @asdek
Created: 4/8/2026
Status: ✅ Merged
Merged: 4/8/2026
Merged by: @asdek
Base:
main← Head:feature/frontend📝 Commits (2)
046f0dbfeat: update terminalf49d9cbfeat: update terminal📊 Changes
8 files changed (+1761 additions, -786 deletions)
View changed files
➖
frontend/src/components/shared/terminal.tsx(+0 -786)➕
frontend/src/components/shared/terminal/index.ts(+2 -0)➕
frontend/src/components/shared/terminal/terminal-config.ts(+100 -0)➕
frontend/src/components/shared/terminal/terminal-sanitizer.test.ts(+728 -0)➕
frontend/src/components/shared/terminal/terminal-sanitizer.ts(+414 -0)➕
frontend/src/components/shared/terminal/terminal.tsx(+92 -0)➕
frontend/src/components/shared/terminal/use-terminal-search.ts(+76 -0)➕
frontend/src/components/shared/terminal/use-xterm.ts(+349 -0)📄 Description
Description of the Change
Problem
The terminal component in PentAGI suffered from three compounding issues:
Correctness — Unicode data was silently destroyed:
The output sanitizer replaced every character outside 7-bit ASCII (including tab/LF/CR exceptions) with a dot. Cyrillic, CJK, Latin Extended, and emoji characters produced by AI agents, pentest tools, or user input were all converted to unintelligible dots. Any non-English text in tool output was unreadable, making the terminal unusable for international content.
Security — Dangerous escape sequences and link protocols were not blocked:
The old sanitizer preserved a broad set of ANSI escape sequences including cursor movement, erase-display, window-title changes (OSC 0/2), and foreground/background color overrides (OSC 10/11/12). An attacker-controlled tool output could visually corrupt the terminal view, forge UI elements, or — through OSC 8 hyperlinks — inject
javascript:ordata:URIs that would execute code if the user clicked them. Unicode bidirectional control characters (RTL override U+202E and related) were also passed through, enabling text-spoofing attacks in log output.Maintainability — Single 786-line file with no tests:
All xterm.js lifecycle management, addon loading, theme synchronization, resize handling, search, log streaming, and sanitization lived in one monolithic component with deeply interleaved concerns. There were no unit tests; a regression in any layer was invisible until it surfaced visually. The scrollback buffer was capped at 2 500 lines (insufficient for long tool runs), and large log batches were written synchronously, risking UI thread stalls.
Solution
Correct Unicode handling in a security-hardened sanitizer (
terminal-sanitizer.ts):The sanitizer is rewritten from scratch with an explicit allowlist approach:
ESC[...m) for color and style — the only escape sequences that legitimately affect how log output looks.http://,https://,mailto:,ssh://,telnet://);javascript:,data:,ftp:,file:and any unknown scheme are blocked.[binary data]if null bytes, lone surrogates, or a high control-character density are found — prevents xterm.js parser errors from binary payloads (JPEG, ELF, etc.).input.slice(safeStart, i)) reduces heap allocations on large inputs from O(n) individual pushes to O(segments); fast-pathneedsSanitizationpre-scan skips the full pass for clean text.Modular component architecture:
The 786-line monolith is split into five focused modules:
terminal-config.ts— static terminal options, dark/light themes, search decoration palettes,isDarkModehelper.terminal-sanitizer.ts— pure sanitization functions with no React dependencies (unit-testable in isolation).use-xterm.ts— full xterm.js lifecycle: terminal creation, addon loading (Fit, Search, Unicode11, WebLinks, WebGL with canvas fallback),requestAnimationFrame-based open to eliminatesetTimeoutraces, resize observer with debounce, theme sync with system media query, and cleanup.use-terminal-search.ts— search reactive hook; encapsulatesfindNext/findPreviousand auto-search-on-value-change logic.terminal.tsx— 92-line component that wires the hooks together and manages only incremental log appending and clear-on-reset logic.Flow control for large log batches (
use-xterm.ts):Large writes are split into 64 KB chunks with xterm's write-callback flow control, preventing the parser from blocking the UI thread on multi-megabyte tool output. Chunk boundaries are aligned to UTF-16 surrogate pairs to avoid splitting emoji across chunks.
Clickable hyperlinks with safe-protocol enforcement:
OSC 8 hyperlinks and plain URLs detected by WebLinksAddon are clickable via Ctrl+Click (Cmd+Click on macOS). A tooltip showing the target URL and the required modifier key appears on hover. The
openLinkhandler validates the protocol against the same safe-protocol list as the sanitizer before callingwindow.open, ensuring no unsafe URI can be opened regardless of how it arrived in the terminal output.Comprehensive test suite (
terminal-sanitizer.test.ts):728 lines of Vitest unit tests covering:
Other improvements:
logLevel: 'off'suppresses xterm.js internal debug output.Closes #
Type of Change
Areas Affected
Testing and Verification
Test Configuration
Test Steps
cd frontend && npm run testcat /usr/bin/ls) — verify[binary data]placeholder appears instead of garbled xterm renderingTest Results
[binary data]without parser errorsjavascript:anddata:URIs blocked;https://links clickablenpm run lintpassedSecurity Considerations
Attack surface reduced:
The previous sanitizer passed through dangerous escape sequences that terminal output could exploit:
javascript:,data:,ftp:,file:URIs cannot be embedded as clickable linksAll these protections apply purely at the sanitizer level before data reaches xterm.js, providing defense in depth even if xterm.js's own parser changes in a future upgrade.
Performance Impact
Improvements:
needsSanitizationpre-scan returns immediately for clean ASCII/Unicode text, avoiding the full sanitizer pass for the majority of log lines.No regressions: the refactored hooks add no additional render cycles; the xterm lifecycle effect still runs once per mount.
Documentation Updates
useXtermhook documenting behavior, security decisions, and references to xterm.js flow control guideDeployment Notes
No configuration changes required. This is a pure frontend refactoring — no new environment variables, no backend changes, no database migrations.
Deployment Steps:
docker compose builddocker compose up -dCompatibility:
Terminalcomponent (logs,searchValue,ref.findNext,ref.findPrevious) is unchangedTerminalReftype is now exported from the module index for use by consumers that need it explicitlyChecklist
Code Quality
go fmtandgo vet(for Go code)npm run lint(for TypeScript/JavaScript code)Security
Compatibility
Documentation
Additional Notes
Key Design Decisions
Why only SGR sequences are preserved from CSI:
CSI SGR (
ESC[...m) is the only sequence class whose effect is purely cosmetic and non-interactive in a read-only log terminal — it changes text color and style but has no side effects on cursor position, buffer state, or terminal geometry. All other CSI sequences (cursor movement, erase, scroll, private mode toggles) manipulate terminal state in ways that would allow crafted output to visually corrupt the display or obscure earlier log lines. Stripping them does not lose semantic information; it merely prevents adversarial rendering.Why
requestAnimationFrameinstead ofsetTimeoutfor terminal open:requestAnimationFramefires after the browser has committed the layout, guaranteeing that the container element has non-zero dimensions whenterminal.open()is called. The previous approach used two nestedsetTimeoutcalls (100 ms + 200 ms), which was a heuristic that could fail under load or on slow devices.cancelAnimationFramein the cleanup function is also semantically correct, whereas the old approach had a race whereclearTimeoutmight not cancel a timer that had already fired.Why
needsSanitizationallows Unicode ≥ U+00A0 by default:The safe-by-default direction was changed from "block everything non-ASCII" to "block only what is explicitly dangerous." Valid Unicode text (Cyrillic, CJK, emoji) is benign in a read-only terminal; the xterm.js Unicode11 addon handles it correctly. Only specific problem categories require intervention: C0/C1 control characters, escape sequences, lone surrogates, U+FFFD, and bidi control characters. This inversion makes the fast path cover the vast majority of real-world tool output without sanitization overhead.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.