/** Column count assumed when the surface cannot report one (not a TTY, a test, a pipe). */ export declare const DEFAULT_FRAME_WIDTH = 80; /** * Narrowest frame the gutter will produce, however small the terminal claims to be. * * Below it the gutter eats the row and the command becomes unreadable, which is its own way of * hiding a payload — so a terminal narrower than this gets a frame it cannot fit rather than no * frame at all. That trade costs the column-0 guarantee, and {@link narrowTerminalNotice} is what * stops it costing it silently. */ export declare const MIN_FRAME_WIDTH = 20; /** * The width to frame at on a terminal reporting `columns`. * * One column is held back because a row that fills the last cell wraps on some terminals, and the * result is floored at {@link MIN_FRAME_WIDTH}. Both surfaces resolve their width here so they * cannot come to disagree about how much of a command a human was shown. */ export declare function frameWidthFor(columns: number | undefined): number; /** * The line a surface must paint when the terminal is too narrow for the frame to fit in it, or * `undefined` when it fits. * * A guarantee that quietly stops holding is worse than one that says it stopped: below the floor * {@link frameWidthFor} keeps the frame readable at the cost of rows wider than the terminal, the * terminal wraps them, and a wrapped row puts untrusted text at column 0 — exactly what the gutter * exists to prevent. The human is told rather than shown a dialog that still looks like it is * guarding them. * * **Deliberately short, and only the consequence.** It is painted on a terminal of twenty columns * or fewer, so it wraps to a few rows however it is worded and every word it does not need is * another row of chrome between the reader and the command they are ruling on. It names what goes * wrong rather than the width that caused it, because the width is not what the reader has to * decide about; the reasoning belongs here, where it has room. */ export declare function narrowTerminalNotice(columns: number | undefined): string | undefined; /** * Body lines shown before {@link frameUntrustedCommand} starts eliding. * * Sized so the extracted-site notices, the body and the menu are on screen **together** on a * conventional 24-row terminal. That matters more than it looks: rule 3 puts the sites above the * body specifically so the eye lands on them, and a body long enough to scroll them off the top * defeats it. Bounding the body is safe here in a way the superseded single-line clamp was not, * because what is elided is chosen *around the flagged sites* and the count of hidden lines is * stated — the payload is what survives, not what is dropped. */ export declare const DEFAULT_FRAME_MAX_LINES = 20; /** * Rows either of the escalation menu's sticky blocks may occupy — what *approve for this session / * always* would remember, and what *always reject* would record. * * Exported so both surfaces bound them identically: two prompts that disagree about how much of a * grant a human was shown is the drift one shared renderer exists to prevent. * * Four, because that is enough for the ordinary case — a one-line command is one row, its JSON * entry two — while keeping an eighteen-line command from printing itself twice more underneath * its own frame and pushing the menu off the screen. See `clampRows` for the measurement. */ export declare const STICKY_PREVIEW_MAX_ROWS = 4; /** * Smallest content width the gutter will leave, whatever the terminal claims. * * Exported alongside {@link wrapToWidth}, so the tests that pin the wrap's floor can state this * term rather than repeat the number and quietly drift from it. */ export declare const MIN_CONTENT_WIDTH = 4; /** What made the gate unable to resolve a command statically, at one position in it. */ export type UntrustedSiteKind = 'command substitution' | 'composition'; /** One extracted decision-relevant position in a command. */ export interface UntrustedSite { /** 1-based line number, matching the gutter the body renders with. */ line: number; /** 1-based display column within that line. */ column: number; kind: UntrustedSiteKind; /** The literal token that was found — `$(`, a backtick, `&&`, `||`, `;` or `|`. */ token: string; /** Neutralised text from the site onward, clipped for the notice line. */ excerpt: string; } export interface FrameOptions { /** Terminal columns available. Rows are wrapped to fit; defaults to {@link DEFAULT_FRAME_WIDTH}. */ width?: number; /** Body lines before elision; defaults to {@link DEFAULT_FRAME_MAX_LINES}. */ maxLines?: number; /** * Terminal ROWS this block may occupy, after wrapping. Unbounded when absent. * * Different from {@link maxLines}, and the difference is the whole reason it exists: `maxLines` * bounds the block's *logical* lines, so a single line long enough to wrap forty times is one * line and passes the budget untouched. What a screen has is rows. * * **It bounds `lines` only — `notices` are outside it**, so a block with extracted sites occupies * this many rows plus its notices. That is not a gap today: `maxRows` is reached through * {@link frameUntrustedText}, which extracts no sites, so those blocks have no notices to bound. * A caller passing it to {@link frameUntrustedCommand} instead would get a bound that does not * bound the whole block, and needs to widen this rather than assume the name. */ maxRows?: number; } /** A block of untrusted text, ready to paint. */ export interface FramedUntrustedText { /** * Renderer-owned lines to paint **above** the body — the extracted sites, and the statement of * anything the elision hid. Empty when there is nothing to say. */ notices: string[]; /** * The body, one terminal row per element, each already carrying its gutter. A surface paints * these verbatim and must not re-wrap them: the gutter is the column-0 guarantee, and a second * wrap would put untrusted bytes back at column 0. */ lines: string[]; /** Every site {@link findUnresolvableSites} found, whether or not its line survived elision. */ sites: UntrustedSite[]; } /** * Replace every control and format character with a visible, printable escape. * * Covers the C0 and C1 control ranges (`\p{Cc}` — which is NUL, BEL, BS, TAB, LF, CR, ESC, DEL and * the 0x80–0x9F block including the one-byte CSI), the format characters (`\p{Cf}` — the bidi * overrides that reorder what a reader sees, the zero-width joiners, the BOM), and the line and * paragraph separators (`\p{Zl}`, `\p{Zp}`). Everything else is left exactly as written: this * neutralises, it never sanitises, so the text a human rules on is the text that was proposed. * * The output alphabet is `\`, `x`, `u`, `{`, `}` and hex digits, none of which is a shell * substitution or composition token — so escaping can neither create nor destroy a site that * {@link findUnresolvableSites} would report. */ export declare function neutralizeUntrustedText(text: string): string; /** * Collapse untrusted text to a single safe line, for the line-structured blocks that carry their * meaning in their line breaks (the §5 negotiation transcript, the rating prompt's own transcript). * * Neutralisation runs **first**: JavaScript's `\s` covers LF, CR and TAB but not ESC, BEL, NUL, the * C1 range or the bidi overrides, so collapsing whitespace alone leaves a screen-clearing sequence * intact on a line that merely looks tidy. */ export declare function neutralizeToOneLine(text: string): string; /** * Every position in `command` at which the gate could not statically resolve it: each command * substitution (`$(` and a backtick) and each composition boundary (`;`, `&&`, `||`, `|`). * * **Deliberately not quote-aware.** A shell-accurate scanner would skip a `$(` inside single quotes * — and the command that motivated this whole surface hid its payload in backticks inside *double* * quotes, where the shell expands it. Over-reporting costs a line of notice; under-reporting is the * incident. The scan is the same order of simplicity as the ambiguity preflight that actually caught * that command, and for the same reason. * * Scanned over the neutralised lines so the reported line and column are the ones the reader can * count to on the screen. */ export declare function findUnresolvableSites(command: string): UntrustedSite[]; /** * Frame the command the agent proposed: neutralised, gutter-numbered, wrapped to the terminal, its * unresolvable sites listed above it, and elided around those sites if it is too long to show whole. */ export declare function frameUntrustedCommand(command: string, options?: FrameOptions): FramedUntrustedText; /** * Frame a block of untrusted prose — the auto-rater's `reason`, or what a sticky choice will store. * * The same gutter and the same neutralisation as a command, and for the same reason: the rater's * explanation is model-authored text on a dialog whose chrome it must not be able to forge. Site * extraction does not run, because prose has no composition boundaries to point at; a block with no * sites that overruns {@link FrameOptions.maxLines} keeps its head and states what it dropped. */ export declare function frameUntrustedText(text: string, options?: FrameOptions): FramedUntrustedText; /** * Split one logical line into terminal rows of at most `width` columns. * * The wrap belongs here rather than to the terminal because a terminal's own wrap starts the * continuation at column 0, which is the forgery this module exists to prevent: a single line long * enough to wrap can otherwise lay attacker-chosen bytes flush-left, gutter and all bypassed. * * Measured and cut with the display-width helpers, never `.length` — a CJK ideograph or an emoji is * one code point in two columns, and a row measured as fitting that does not fit is a row the * terminal wraps back to column 0. Conservatively, so the same holds on a terminal that renders * Ambiguous characters wide. * * The budget is floored at {@link MIN_CONTENT_WIDTH} here rather than taken on trust. The * over-wide-cluster branch below escapes a cluster it cannot draw and re-measures it, which * advances only if the budget can hold at least one escape character; at a budget of zero it * re-escapes its own backslashes forever and the row never terminates. `frame` does pass a * floored width, but a loop that terminates because of what its only caller happens to do is one * the next caller silently breaks. * * Exported for tests, which pin that termination at a degenerate width — the one width the public * entry points cannot produce. */ export declare function wrapToWidth(text: string, width: number): string[];