Documentation
¶
Overview ¶
Package sanitize is the one output chokepoint for terminal-unsafe text (the August 2026 chat-rebuild audit, no longer in the tree, Part 10.2 items 6-7).
Anything that reaches the terminal and was not authored by our own renderer — a model reply, a tool result, a worker trace line — is a channel someone else's bytes can drive. Escape sequences are how a terminal emulator is remote-controlled: OSC 52 writes the system clipboard, OSC 0/1/2 rewrite the window/tab title, OSC 8 can point a clickable span at an arbitrary URL, and the DCS/APC/PM/SOS string types exist for the same reason CSI does — to make the emulator DO something, not print something. Sanitize.Text (and its palette-remapping sibling, TextWithPalette) is the single place that content gets to cross from "somebody else's bytes" to "safe to draw."
What survives (the allowlist) ¶
- plain text, including all valid UTF-8 beyond ASCII
- '\n' and '\t'
- SGR sequences: CSI parameter-string 'm' (ESC '[' ... 'm'), the color and style codes lipgloss/our renderers already emit. Nothing else introduced by ESC survives — cursor movement, screen/line erase, mode-setting, and every other CSI final byte are stripped, along with both 2- and 3-byte ESC forms (ESC 'c', ESC '(' 'B', ...).
What is neutralized ¶
OSC of any kind (52 clipboard, 0/1/2 title, 8 hyperlink, 7/9/133/777/99 and anything unrecognized — the list is not an allowlist, so a new OSC number needs no code change here), DCS/APC/PM/SOS strings, every C1 control byte (0x80-0x9F, both as a raw byte and via the classic ESC-prefixed 7-bit forms), and any C0 control other than '\n'/'\t'. Malformed input (a truncated OSC, a lone trailing ESC, an escape nested inside another escape's payload, invalid UTF-8) is consumed defensively and never panics — see the fuzz corpus in sanitize_test.go, written after this codebase's escape-blind-truncate incident (internal/tui's ANSI-aware truncate/wrapText).
OSC 8 policy ¶
The product itself adopts OSC 8 hyperlinks (audit notes 5.21) — the threat is not the sequence, it's letting UNTRUSTED text mint one. So OSC 8 arriving inside model/tool/trace output is stripped like any other OSC, leaving the visible link text behind; our own renderer is the only thing allowed to wrap a span in a real OSC 8 link, and it does so after this chokepoint, from our own chrome strings, never from sanitized content.
Palette remap (10.2.7) ¶
TextWithPalette additionally remaps raw ANSI-16 SGR color codes (the classic 30-37/40-47/90-97/100-107 families) through a caller-supplied Table, so a tool's own colors cohere with ours instead of clashing with whatever the token layer eventually defines (Wave 2). The mechanism lands now; Identity is the only table anyone plugs in until then, and calling with Identity is a single 16-int array compare — no extra allocation, no second pass over the text.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Identity = Table{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
Identity is the zero-cost default palette: every color maps to itself. Wave 2's token layer plugs a real Table in here; until then every caller either omits the palette (Text) or passes Identity explicitly.
Functions ¶
func Text ¶
Text returns s with every terminal-executable sequence neutralized, leaving SGR styling, plain text, and newlines/tabs untouched. Benign input — the overwhelming common case on the render hot path — costs one linear scan for an ESC or C1 byte and returns the original string with no allocation: the fast-path check lives directly in Text/TextWithPalette, not inside render, specifically so the common case never enters a function whose body declares a strings.Builder (see render's doc comment) and therefore never pays for one, even in the failure mode where the compiler's escape analysis can't prove that builder stack-allocatable.
func TextWithPalette ¶
TextWithPalette sanitizes s exactly like Text and, in the same pass, remaps ANSI-16 SGR color codes through t. Passing Identity is exactly as cheap as calling Text.
Types ¶
type Table ¶
type Table [16]int
Table remaps the 16 classic ANSI SGR colors (indices 0-7 standard, 8-15 bright) onto replacement color indices. Table[i] is the index tool output's color i is redrawn as; Identity leaves every color where it found it.
func (Table) IsIdentity ¶
IsIdentity reports whether t performs no remapping. Callers holding a Table from settings/config can use this to skip TextWithPalette entirely and call Text instead — the two are behaviorally identical for an identity table, but this avoids even the one array-compare per call.