@dreb/coding-agent 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ import { TerminalTextRender } from "terminal-render";
2
+ /**
3
+ * Maximum row or column value allowed in ANSI cursor positioning sequences.
4
+ * Anything larger gets capped to this value to prevent memory exhaustion from
5
+ * malicious sequences like `ESC[9999999;1H`.
6
+ */
7
+ const MAX_CURSOR_POSITION = 5000;
8
+ /**
9
+ * Sanitize ANSI cursor positioning sequences to prevent memory exhaustion.
10
+ *
11
+ * Sequences like `ESC[9999999;1H` can cause TerminalTextRender to allocate
12
+ * millions of empty lines. This function caps row/column values in:
13
+ * - CUP (cursor position): ESC[<row>;<col>H or ESC[<row>;<col>f
14
+ * - CUU/CUD/CUF/CUB (cursor movement): ESC[<n>A/B/C/D
15
+ * - CNL (cursor next line): ESC[<n>E
16
+ * - VPA (vertical position absolute): ESC[<row>d
17
+ * - HPA (horizontal position absolute): ESC[<col>G or ESC[<col>`
18
+ *
19
+ * In addition to per-sequence caps, this tracks cumulative cursor position
20
+ * to prevent accumulation attacks where many sequences each at the per-sequence
21
+ * cap combine to push the cursor to millions of rows/columns, triggering OOM
22
+ * via array allocation in the terminal renderer.
23
+ */
24
+ export function sanitizeCursorPositioning(input) {
25
+ // Track cumulative cursor position across sequences. An attacker can send
26
+ // thousands of ESC[5000B sequences, each passing the per-sequence cap but
27
+ // accumulating to ~55M rows. By tracking position, we clamp movement that
28
+ // would exceed the limit.
29
+ let cursorRow = 0;
30
+ let cursorCol = 0;
31
+ const parsePart = (p) => {
32
+ if (p === undefined)
33
+ return null;
34
+ const n = Number.parseInt(p, 10);
35
+ return Number.isNaN(n) ? null : n;
36
+ };
37
+ // Cap all numeric params individually and rebuild the param string
38
+ const capParams = (rawParts) => rawParts
39
+ .map((p) => {
40
+ const n = Number.parseInt(p, 10);
41
+ if (Number.isNaN(n))
42
+ return p;
43
+ return String(Math.min(n, MAX_CURSOR_POSITION));
44
+ })
45
+ .join(";");
46
+ // Match CSI sequences: ESC[ followed by params and a final byte
47
+ // Covers H, f (CUP), A/B/C/D (movement), E (CNL), d (VPA), G/` (HPA), r (scroll region)
48
+ return input.replace(/\x1b\[([0-9;]*)([ABCDEGHfdr`])/g, (_match, params, cmd) => {
49
+ const rawParts = params.split(";");
50
+ switch (cmd) {
51
+ case "B": // Cursor down by n (default 1)
52
+ case "E": {
53
+ // Cursor next line by n (default 1)
54
+ const n = parsePart(rawParts[0]) ?? 1;
55
+ const capped = Math.min(n, MAX_CURSOR_POSITION);
56
+ const allowed = Math.max(0, MAX_CURSOR_POSITION - cursorRow);
57
+ const clamped = Math.min(capped, allowed);
58
+ cursorRow += clamped;
59
+ if (cmd === "E")
60
+ cursorCol = 0;
61
+ return `\x1b[${clamped}${cmd}`;
62
+ }
63
+ case "A": {
64
+ // Cursor up by n (default 1)
65
+ const n = parsePart(rawParts[0]) ?? 1;
66
+ const capped = Math.min(n, MAX_CURSOR_POSITION);
67
+ cursorRow = Math.max(0, cursorRow - capped);
68
+ return `\x1b[${capped}A`;
69
+ }
70
+ case "C": {
71
+ // Cursor forward by n (default 1)
72
+ const n = parsePart(rawParts[0]) ?? 1;
73
+ const capped = Math.min(n, MAX_CURSOR_POSITION);
74
+ const allowed = Math.max(0, MAX_CURSOR_POSITION - cursorCol);
75
+ const clamped = Math.min(capped, allowed);
76
+ cursorCol += clamped;
77
+ return `\x1b[${clamped}C`;
78
+ }
79
+ case "D": {
80
+ // Cursor back by n (default 1)
81
+ const n = parsePart(rawParts[0]) ?? 1;
82
+ const capped = Math.min(n, MAX_CURSOR_POSITION);
83
+ cursorCol = Math.max(0, cursorCol - capped);
84
+ return `\x1b[${capped}D`;
85
+ }
86
+ case "H":
87
+ case "f": {
88
+ // Absolute cursor position: ESC[row;colH
89
+ const row = parsePart(rawParts[0]);
90
+ const col = parsePart(rawParts[1]);
91
+ cursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;
92
+ cursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;
93
+ return `\x1b[${capParams(rawParts)}${cmd}`;
94
+ }
95
+ case "d": {
96
+ // VPA - vertical position absolute
97
+ const row = parsePart(rawParts[0]);
98
+ cursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;
99
+ return `\x1b[${capParams(rawParts)}d`;
100
+ }
101
+ case "G":
102
+ case "`": {
103
+ // HPA - horizontal position absolute
104
+ const col = parsePart(rawParts[0]);
105
+ cursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;
106
+ return `\x1b[${capParams(rawParts)}${cmd}`;
107
+ }
108
+ case "r": {
109
+ // Set scroll region - cap params, no position tracking
110
+ return `\x1b[${capParams(rawParts)}r`;
111
+ }
112
+ default: {
113
+ return `\x1b[${capParams(rawParts)}${cmd}`;
114
+ }
115
+ }
116
+ });
117
+ }
118
+ /**
119
+ * Process raw terminal output through a terminal renderer, producing the clean
120
+ * text a human would actually see on screen.
121
+ *
122
+ * This handles:
123
+ * - Carriage returns (`\r`) — progress bars overwrite the current line
124
+ * - ANSI cursor movement — up, down, forward, backward, absolute positioning
125
+ * - Backspace (`\b`) — moves cursor back one position
126
+ * - Line clearing / screen clearing escape sequences
127
+ * - Tab stops
128
+ *
129
+ * The result is the final rendered state of the terminal — identical to what
130
+ * a human would see in a real terminal after the output completes.
131
+ *
132
+ * Safety:
133
+ * - Cursor positioning values are capped to prevent memory exhaustion
134
+ * - Errors fall back to returning the raw input
135
+ */
136
+ export function renderTerminalOutput(raw) {
137
+ if (!raw)
138
+ return raw;
139
+ try {
140
+ const sanitized = sanitizeCursorPositioning(raw);
141
+ const renderer = new TerminalTextRender();
142
+ renderer.write(sanitized);
143
+ return renderer.render();
144
+ }
145
+ catch (err) {
146
+ const detail = err instanceof Error ? err.message : String(err);
147
+ console.error(`[dreb] terminal-render fallback: TerminalTextRender failed (${detail}), returning raw output`);
148
+ return raw;
149
+ }
150
+ }
151
+ //# sourceMappingURL=terminal-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-render.js","sourceRoot":"","sources":["../../../src/core/tools/terminal-render.ts"],"names":[],"mappings":"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","sourcesContent":["import { TerminalTextRender } from \"terminal-render\";\n\n/**\n * Maximum row or column value allowed in ANSI cursor positioning sequences.\n * Anything larger gets capped to this value to prevent memory exhaustion from\n * malicious sequences like `ESC[9999999;1H`.\n */\nconst MAX_CURSOR_POSITION = 5000;\n\n/**\n * Sanitize ANSI cursor positioning sequences to prevent memory exhaustion.\n *\n * Sequences like `ESC[9999999;1H` can cause TerminalTextRender to allocate\n * millions of empty lines. This function caps row/column values in:\n * - CUP (cursor position): ESC[<row>;<col>H or ESC[<row>;<col>f\n * - CUU/CUD/CUF/CUB (cursor movement): ESC[<n>A/B/C/D\n * - CNL (cursor next line): ESC[<n>E\n * - VPA (vertical position absolute): ESC[<row>d\n * - HPA (horizontal position absolute): ESC[<col>G or ESC[<col>`\n *\n * In addition to per-sequence caps, this tracks cumulative cursor position\n * to prevent accumulation attacks where many sequences each at the per-sequence\n * cap combine to push the cursor to millions of rows/columns, triggering OOM\n * via array allocation in the terminal renderer.\n */\nexport function sanitizeCursorPositioning(input: string): string {\n\t// Track cumulative cursor position across sequences. An attacker can send\n\t// thousands of ESC[5000B sequences, each passing the per-sequence cap but\n\t// accumulating to ~55M rows. By tracking position, we clamp movement that\n\t// would exceed the limit.\n\tlet cursorRow = 0;\n\tlet cursorCol = 0;\n\n\tconst parsePart = (p: string | undefined): number | null => {\n\t\tif (p === undefined) return null;\n\t\tconst n = Number.parseInt(p, 10);\n\t\treturn Number.isNaN(n) ? null : n;\n\t};\n\n\t// Cap all numeric params individually and rebuild the param string\n\tconst capParams = (rawParts: string[]) =>\n\t\trawParts\n\t\t\t.map((p: string) => {\n\t\t\t\tconst n = Number.parseInt(p, 10);\n\t\t\t\tif (Number.isNaN(n)) return p;\n\t\t\t\treturn String(Math.min(n, MAX_CURSOR_POSITION));\n\t\t\t})\n\t\t\t.join(\";\");\n\n\t// Match CSI sequences: ESC[ followed by params and a final byte\n\t// Covers H, f (CUP), A/B/C/D (movement), E (CNL), d (VPA), G/` (HPA), r (scroll region)\n\treturn input.replace(/\\x1b\\[([0-9;]*)([ABCDEGHfdr`])/g, (_match, params: string, cmd: string) => {\n\t\tconst rawParts = params.split(\";\");\n\n\t\tswitch (cmd) {\n\t\t\tcase \"B\": // Cursor down by n (default 1)\n\t\t\tcase \"E\": {\n\t\t\t\t// Cursor next line by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tconst allowed = Math.max(0, MAX_CURSOR_POSITION - cursorRow);\n\t\t\t\tconst clamped = Math.min(capped, allowed);\n\t\t\t\tcursorRow += clamped;\n\t\t\t\tif (cmd === \"E\") cursorCol = 0;\n\t\t\t\treturn `\\x1b[${clamped}${cmd}`;\n\t\t\t}\n\t\t\tcase \"A\": {\n\t\t\t\t// Cursor up by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tcursorRow = Math.max(0, cursorRow - capped);\n\t\t\t\treturn `\\x1b[${capped}A`;\n\t\t\t}\n\t\t\tcase \"C\": {\n\t\t\t\t// Cursor forward by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tconst allowed = Math.max(0, MAX_CURSOR_POSITION - cursorCol);\n\t\t\t\tconst clamped = Math.min(capped, allowed);\n\t\t\t\tcursorCol += clamped;\n\t\t\t\treturn `\\x1b[${clamped}C`;\n\t\t\t}\n\t\t\tcase \"D\": {\n\t\t\t\t// Cursor back by n (default 1)\n\t\t\t\tconst n = parsePart(rawParts[0]) ?? 1;\n\t\t\t\tconst capped = Math.min(n, MAX_CURSOR_POSITION);\n\t\t\t\tcursorCol = Math.max(0, cursorCol - capped);\n\t\t\t\treturn `\\x1b[${capped}D`;\n\t\t\t}\n\t\t\tcase \"H\":\n\t\t\tcase \"f\": {\n\t\t\t\t// Absolute cursor position: ESC[row;colH\n\t\t\t\tconst row = parsePart(rawParts[0]);\n\t\t\t\tconst col = parsePart(rawParts[1]);\n\t\t\t\tcursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;\n\t\t\t\tcursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}${cmd}`;\n\t\t\t}\n\t\t\tcase \"d\": {\n\t\t\t\t// VPA - vertical position absolute\n\t\t\t\tconst row = parsePart(rawParts[0]);\n\t\t\t\tcursorRow = row != null ? Math.min(row, MAX_CURSOR_POSITION) : 1;\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}d`;\n\t\t\t}\n\t\t\tcase \"G\":\n\t\t\tcase \"`\": {\n\t\t\t\t// HPA - horizontal position absolute\n\t\t\t\tconst col = parsePart(rawParts[0]);\n\t\t\t\tcursorCol = col != null ? Math.min(col, MAX_CURSOR_POSITION) : 1;\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}${cmd}`;\n\t\t\t}\n\t\t\tcase \"r\": {\n\t\t\t\t// Set scroll region - cap params, no position tracking\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}r`;\n\t\t\t}\n\t\t\tdefault: {\n\t\t\t\treturn `\\x1b[${capParams(rawParts)}${cmd}`;\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Process raw terminal output through a terminal renderer, producing the clean\n * text a human would actually see on screen.\n *\n * This handles:\n * - Carriage returns (`\\r`) — progress bars overwrite the current line\n * - ANSI cursor movement — up, down, forward, backward, absolute positioning\n * - Backspace (`\\b`) — moves cursor back one position\n * - Line clearing / screen clearing escape sequences\n * - Tab stops\n *\n * The result is the final rendered state of the terminal — identical to what\n * a human would see in a real terminal after the output completes.\n *\n * Safety:\n * - Cursor positioning values are capped to prevent memory exhaustion\n * - Errors fall back to returning the raw input\n */\nexport function renderTerminalOutput(raw: string): string {\n\tif (!raw) return raw;\n\ttry {\n\t\tconst sanitized = sanitizeCursorPositioning(raw);\n\t\tconst renderer = new TerminalTextRender();\n\t\trenderer.write(sanitized);\n\t\treturn renderer.render();\n\t} catch (err) {\n\t\tconst detail = err instanceof Error ? err.message : String(err);\n\t\tconsole.error(`[dreb] terminal-render fallback: TerminalTextRender failed (${detail}), returning raw output`);\n\t\treturn raw;\n\t}\n}\n"]}