pi-agent: Implement final TUI hardening
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user