pi-agent: Implement final TUI hardening

This commit is contained in:
proxyctl coordinator
2026-07-18 01:07:54 +08:00
parent 954e7d977a
commit f15ffc2301
10 changed files with 598 additions and 39 deletions
+92 -11
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/charmbracelet/bubbles/viewport"
@@ -102,6 +103,8 @@ type terminalPage struct {
contextOffset int
contextAnchor int
anchorContext bool
wrapPrimary bool
wrapContext bool
dashboard *dashboardRenderState
}
@@ -150,9 +153,9 @@ func (p terminalPresentation) Render(state presentationState) string {
// frames use the stricter boundary that removes the entire userinfo.
frame := p.renderShell(page, width, height)
if state.page == formPage {
return profile.RedactUserinfoText(frame)
return sanitizeStyledText(profile.RedactUserinfoText(frame))
}
return profile.RedactText(frame)
return sanitizeStyledText(profile.RedactText(frame))
}
func renderStatus(state presentationState) string {
@@ -175,14 +178,84 @@ func renderStatus(state presentationState) string {
}
func redactTerminalPage(page terminalPage) terminalPage {
page.header = profile.RedactText(page.header)
page.primary = profile.RedactText(page.primary)
page.context = profile.RedactText(page.context)
page.footer = profile.RedactText(page.footer)
page.status = profile.RedactText(page.status)
page.header = protectRenderedText(page.header)
page.primary = protectRenderedText(page.primary)
page.context = protectRenderedText(page.context)
page.footer = protectRenderedText(page.footer)
page.status = protectRenderedText(page.status)
return page
}
func protectRenderedText(value string) string {
return sanitizeStyledText(profile.RedactText(value))
}
var safeSGRPattern = regexp.MustCompile(`^(?:0|(?:(?:1;)?(?:4;)?38;5;(?:25|34|75|160|172|235|242|245|252)))$`)
// sanitizeStyledText removes terminal-control injection while retaining only
// the small SGR palette emitted by semanticTheme. OSC title/hyperlink commands,
// cursor movement, concealment, carriage returns, and private C1 controls are
// never meaningful application text and must not reach a rendered frame.
func sanitizeStyledText(value string) string {
var safe strings.Builder
for i := 0; i < len(value); {
if value[i] == '\x1b' {
if i+1 < len(value) && value[i+1] == '[' {
end := i + 2
for end < len(value) && (value[end] < 0x40 || value[end] > 0x7e) {
end++
}
if end < len(value) {
if value[end] == 'm' && safeSGRPattern.MatchString(value[i+2:end]) {
safe.WriteString(value[i : end+1])
}
i = end + 1
continue
}
i = len(value)
continue
}
if i+1 < len(value) && value[i+1] == ']' {
i += 2
for i < len(value) {
if value[i] == '\a' {
i++
break
}
if value[i] == '\x1b' && i+1 < len(value) && value[i+1] == '\\' {
i += 2
break
}
i++
}
continue
}
i++
continue
}
switch value[i] {
case '\n':
safe.WriteByte('\n')
case '\t':
safe.WriteString(" ")
default:
if value[i] >= 0x20 && value[i] != 0x7f {
safe.WriteByte(value[i])
}
}
i++
}
var visible strings.Builder
for _, r := range safe.String() {
if (r >= 0x80 && r <= 0x9f) || (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069) {
continue
}
visible.WriteRune(r)
}
return visible.String()
}
func (p terminalPresentation) renderShell(page terminalPage, width, height int) string {
header := viewportContent(page.header, width, lineCount(page.header), 0)
footer := viewportContent(page.footer, width, lineCount(page.footer), 0)
@@ -232,22 +305,30 @@ func (p terminalPresentation) renderShell(page terminalPage, width, height int)
}
func renderPageBody(page terminalPage, width, height int) string {
primaryContent := page.primary
if page.wrapPrimary {
primaryContent = lipgloss.NewStyle().Width(width).Render(primaryContent)
}
if page.context == "" || height < 5 {
return viewportContentAt(page.primary, width, height, page.primaryOffset, page.focusLine)
return viewportContentAt(primaryContent, width, height, page.primaryOffset, page.focusLine)
}
contextHeight := min(lineCount(page.context), max(3, height/2))
context := page.context
if page.wrapContext {
context = lipgloss.NewStyle().Width(width).Render(context)
}
contextHeight := min(lineCount(context), max(3, height/2))
primaryHeight := height - contextHeight - 1
if primaryHeight < 1 {
primaryHeight = 1
contextHeight = max(1, height-primaryHeight-1)
}
primary := viewportContentAt(page.primary, width, primaryHeight, page.primaryOffset, page.focusLine)
primary := viewportContentAt(primaryContent, width, primaryHeight, page.primaryOffset, page.focusLine)
contextFocus := -1
if page.anchorContext {
contextFocus = page.contextAnchor
}
context := viewportContentAt(page.context, width, contextHeight, page.contextOffset, contextFocus)
context = viewportContentAt(context, width, contextHeight, page.contextOffset, contextFocus)
return primary + "\n\n" + context
}