From f282d25cd8d9863e6a90b17d097c2152601c01cc Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:30:21 +0200 Subject: [PATCH 1/3] fix(python-sdk): port current JS stripAnsi regex to strip_ansi_escape_codes The Python port still used the old ansi-regex pattern while the JS SDK was rewritten (#895) to match OSC sequences non-greedily up to the first string terminator and CSI sequences without requiring one. Aligns both implementations and mirrors the 14-case Python test suite into the JS SDK, which had no stripAnsi tests. Co-Authored-By: Claude Fable 5 --- .changeset/python-strip-ansi-port.md | 5 ++ packages/js-sdk/tests/stripAnsi.test.ts | 62 +++++++++++++++++++ packages/python-sdk/e2b/template/utils.py | 13 ++-- .../utils/test_strip_ansi_escape_codes.py | 33 ++++++++++ 4 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 .changeset/python-strip-ansi-port.md create mode 100644 packages/js-sdk/tests/stripAnsi.test.ts diff --git a/.changeset/python-strip-ansi-port.md b/.changeset/python-strip-ansi-port.md new file mode 100644 index 0000000000..dab666db38 --- /dev/null +++ b/.changeset/python-strip-ansi-port.md @@ -0,0 +1,5 @@ +--- +"@e2b/python-sdk": patch +--- + +Port the JS SDK's `stripAnsi` regex to the Python SDK's `strip_ansi_escape_codes`, aligning both implementations. OSC sequences (hyperlinks, window titles) are now matched non-greedily up to the first string terminator — including sequences spanning newlines — and CSI sequences are stripped without requiring a terminator, so template build logs are cleaned identically in both SDKs. diff --git a/packages/js-sdk/tests/stripAnsi.test.ts b/packages/js-sdk/tests/stripAnsi.test.ts new file mode 100644 index 0000000000..08ef5585cc --- /dev/null +++ b/packages/js-sdk/tests/stripAnsi.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from 'vitest' +import { stripAnsi } from '../src/utils' + +// Mirrors packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py + +test('strips basic SGR color', () => { + expect(stripAnsi('\x1b[31mred\x1b[0m')).toBe('red') +}) + +test('strips semicolon-separated params', () => { + expect(stripAnsi('\x1b[1;31;42mhi\x1b[0m')).toBe('hi') +}) + +test('strips semicolon 256-color', () => { + expect(stripAnsi('\x1b[38;5;82mX\x1b[0m')).toBe('X') +}) + +test('strips colon 256-color', () => { + expect(stripAnsi('\x1b[38:5:82mX\x1b[0m')).toBe('X') +}) + +test('strips colon truecolor', () => { + expect(stripAnsi('\x1b[38:2::255:0:0mRED\x1b[0m')).toBe('RED') +}) + +test('strips colon curly underline', () => { + expect(stripAnsi('\x1b[4:3mX\x1b[0m')).toBe('X') +}) + +test('leaves plain text unchanged', () => { + expect(stripAnsi('no escape codes here')).toBe('no escape codes here') +}) + +test('strips OSC hyperlink', () => { + expect(stripAnsi('\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07')).toBe('E2B') +}) + +test('strips OSC window title (BEL terminated)', () => { + expect(stripAnsi('\x1b]0;my title\x07text')).toBe('text') +}) + +test('strips OSC window title (ESC backslash terminated)', () => { + expect(stripAnsi('\x1b]0;my title\x1b\\text')).toBe('text') +}) + +test('strips OSC spanning newlines', () => { + expect(stripAnsi('\x1b]0;line1\nline2\x07after')).toBe('after') +}) + +test('strips only to the first OSC terminator', () => { + expect(stripAnsi('\x1b]0;title\x07middle\x1b]0;other\x07end')).toBe( + 'middleend' + ) +}) + +test('strips cursor movement', () => { + expect(stripAnsi('\x1b[2Ax\x1b[1000Dy')).toBe('xy') +}) + +test('strips erase line', () => { + expect(stripAnsi('\x1b[2Kdone')).toBe('done') +}) diff --git a/packages/python-sdk/e2b/template/utils.py b/packages/python-sdk/e2b/template/utils.py index fc54936b1c..bb97ca41ed 100644 --- a/packages/python-sdk/e2b/template/utils.py +++ b/packages/python-sdk/e2b/template/utils.py @@ -316,11 +316,14 @@ def strip_ansi_escape_codes(text: str) -> str: """ # Valid string terminator sequences are BEL, ESC\, and 0x9c st = r"(?:\u0007|\u001B\u005C|\u009C)" - pattern = [ - rf"[\u001B\u009B][\[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?{st})", - r"(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))", - ] - ansi_escape = re.compile("|".join(pattern), re.UNICODE) + # OSC sequences only: ESC ] ... ST (non-greedy until the first ST) + osc = rf"(?:\u001B\][\s\S]*?{st})" + # CSI and related: ESC/C1, optional intermediates, optional params + # (supports ; and :) then final byte + csi = ( + r"[\u001B\u009B][\[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]" + ) + ansi_escape = re.compile(f"{osc}|{csi}") return ansi_escape.sub("", text) diff --git a/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py b/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py index d0874ad6ab..4a653c2e92 100644 --- a/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py +++ b/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py @@ -27,3 +27,36 @@ def test_strips_colon_curly_underline(): def test_leaves_plain_text_unchanged(): assert strip_ansi_escape_codes("no escape codes here") == "no escape codes here" + + +def test_strips_osc_hyperlink(): + assert ( + strip_ansi_escape_codes("\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07") == "E2B" + ) + + +def test_strips_osc_window_title_bel_terminated(): + assert strip_ansi_escape_codes("\x1b]0;my title\x07text") == "text" + + +def test_strips_osc_esc_backslash_terminated(): + assert strip_ansi_escape_codes("\x1b]0;my title\x1b\\text") == "text" + + +def test_strips_osc_spanning_newlines(): + assert strip_ansi_escape_codes("\x1b]0;line1\nline2\x07after") == "after" + + +def test_strips_only_first_osc_terminator(): + assert ( + strip_ansi_escape_codes("\x1b]0;title\x07middle\x1b]0;other\x07end") + == "middleend" + ) + + +def test_strips_cursor_movement(): + assert strip_ansi_escape_codes("\x1b[2Ax\x1b[1000Dy") == "xy" + + +def test_strips_erase_line(): + assert strip_ansi_escape_codes("\x1b[2Kdone") == "done" From 9bce30ffabb743aca057df0faa0770045575a6c9 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:50:01 +0200 Subject: [PATCH 2/3] fix(sdks): strip DCS/SOS/PM/APC string controls in stripAnsi Addresses PR review feedback: the OSC-only branch regressed vs the old Python pattern, which stripped ST-terminated string controls like DCS (Sixel, tmux passthrough) through their terminator. Widen the branch to all ECMA-48 string controls (ESC ]/P/X/^/_ ... ST) in both the JS and Python implementations, keeping them byte-for-byte identical. Co-Authored-By: Claude Fable 5 --- .changeset/python-strip-ansi-port.md | 3 ++- packages/js-sdk/src/utils.ts | 6 ++--- packages/js-sdk/tests/stripAnsi.test.ts | 23 ++++++++++++++++++ packages/python-sdk/e2b/template/utils.py | 7 +++--- .../utils/test_strip_ansi_escape_codes.py | 24 +++++++++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/.changeset/python-strip-ansi-port.md b/.changeset/python-strip-ansi-port.md index dab666db38..3bb7e59cfb 100644 --- a/.changeset/python-strip-ansi-port.md +++ b/.changeset/python-strip-ansi-port.md @@ -1,5 +1,6 @@ --- +"e2b": patch "@e2b/python-sdk": patch --- -Port the JS SDK's `stripAnsi` regex to the Python SDK's `strip_ansi_escape_codes`, aligning both implementations. OSC sequences (hyperlinks, window titles) are now matched non-greedily up to the first string terminator — including sequences spanning newlines — and CSI sequences are stripped without requiring a terminator, so template build logs are cleaned identically in both SDKs. +Align ANSI stripping of template build log messages across both SDKs. The Python SDK's `strip_ansi_escape_codes` now ports the JS SDK's `stripAnsi` regex: OSC sequences (hyperlinks, window titles) are matched non-greedily up to the first string terminator — including sequences spanning newlines — and CSI sequences are stripped without requiring a terminator. Both implementations additionally strip the remaining ECMA-48 string controls (DCS/Sixel, SOS, PM, APC) through their string terminator so control payloads no longer leak into cleaned logs. diff --git a/packages/js-sdk/src/utils.ts b/packages/js-sdk/src/utils.ts index 245b4e411b..a1ff16e67e 100644 --- a/packages/js-sdk/src/utils.ts +++ b/packages/js-sdk/src/utils.ts @@ -88,13 +88,13 @@ export async function dynamicImport(module: string): Promise { function ansiRegex({ onlyFirst = false } = {}) { // Valid string terminator sequences are BEL, ESC\, and 0x9c const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)' - // OSC sequences only: ESC ] ... ST (non-greedy until the first ST) - const osc = `(?:\\u001B\\][\\s\\S]*?${ST})` + // String controls (OSC, DCS, SOS, PM, APC): ESC ]/P/X/^/_ ... ST (non-greedy until the first ST) + const strings = `(?:\\u001B[\\]PX^_][\\s\\S]*?${ST})` // CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte const csi = '[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]' - const pattern = `${osc}|${csi}` + const pattern = `${strings}|${csi}` return new RegExp(pattern, onlyFirst ? undefined : 'g') } diff --git a/packages/js-sdk/tests/stripAnsi.test.ts b/packages/js-sdk/tests/stripAnsi.test.ts index 08ef5585cc..72d04bb3cc 100644 --- a/packages/js-sdk/tests/stripAnsi.test.ts +++ b/packages/js-sdk/tests/stripAnsi.test.ts @@ -60,3 +60,26 @@ test('strips cursor movement', () => { test('strips erase line', () => { expect(stripAnsi('\x1b[2Kdone')).toBe('done') }) +test('strips DCS (ESC backslash terminated)', () => { + expect(stripAnsi('\x1bPabc\x1b\\done')).toBe('done') +}) + +test('strips DCS sixel payload', () => { + expect(stripAnsi('\x1bPq#0;2;0;0;0~~@@\x1b\\image')).toBe('image') +}) + +test('strips SOS', () => { + expect(stripAnsi('\x1bXhidden\x1b\\ok')).toBe('ok') +}) + +test('strips PM', () => { + expect(stripAnsi('\x1b^private msg\x1b\\ok')).toBe('ok') +}) + +test('strips APC', () => { + expect(stripAnsi('\x1b_app command\x07ok')).toBe('ok') +}) + +test('strips only the intro of an unterminated DCS', () => { + expect(stripAnsi('\x1bPno terminator here')).toBe('no terminator here') +}) diff --git a/packages/python-sdk/e2b/template/utils.py b/packages/python-sdk/e2b/template/utils.py index bb97ca41ed..a2fe70dfdb 100644 --- a/packages/python-sdk/e2b/template/utils.py +++ b/packages/python-sdk/e2b/template/utils.py @@ -316,14 +316,15 @@ def strip_ansi_escape_codes(text: str) -> str: """ # Valid string terminator sequences are BEL, ESC\, and 0x9c st = r"(?:\u0007|\u001B\u005C|\u009C)" - # OSC sequences only: ESC ] ... ST (non-greedy until the first ST) - osc = rf"(?:\u001B\][\s\S]*?{st})" + # String controls (OSC, DCS, SOS, PM, APC): ESC ]/P/X/^/_ ... ST + # (non-greedy until the first ST) + strings = rf"(?:\u001B[\]PX^_][\s\S]*?{st})" # CSI and related: ESC/C1, optional intermediates, optional params # (supports ; and :) then final byte csi = ( r"[\u001B\u009B][\[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]" ) - ansi_escape = re.compile(f"{osc}|{csi}") + ansi_escape = re.compile(f"{strings}|{csi}") return ansi_escape.sub("", text) diff --git a/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py b/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py index 4a653c2e92..52299c9830 100644 --- a/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py +++ b/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py @@ -60,3 +60,27 @@ def test_strips_cursor_movement(): def test_strips_erase_line(): assert strip_ansi_escape_codes("\x1b[2Kdone") == "done" + + +def test_strips_dcs_esc_backslash_terminated(): + assert strip_ansi_escape_codes("\x1bPabc\x1b\\done") == "done" + + +def test_strips_dcs_sixel_payload(): + assert strip_ansi_escape_codes("\x1bPq#0;2;0;0;0~~@@\x1b\\image") == "image" + + +def test_strips_sos(): + assert strip_ansi_escape_codes("\x1bXhidden\x1b\\ok") == "ok" + + +def test_strips_pm(): + assert strip_ansi_escape_codes("\x1b^private msg\x1b\\ok") == "ok" + + +def test_strips_apc(): + assert strip_ansi_escape_codes("\x1b_app command\x07ok") == "ok" + + +def test_strips_unterminated_dcs_intro_only(): + assert strip_ansi_escape_codes("\x1bPno terminator here") == "no terminator here" From 2a058f71ee14784b17595fe844b2867ffc33ce64 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:01:57 +0200 Subject: [PATCH 3/3] fix(python-sdk): compile stripAnsi regex with re.ASCII for JS parity Python's \d matches any Unicode Nd digit by default, so malformed CSI sequences with e.g. Arabic-Indic digits were stripped in Python but kept by JS, whose \d is always ASCII-only. re.ASCII restores byte-for-byte parity; [\s\S] is unaffected. Addresses PR review. Co-Authored-By: Claude Fable 5 --- packages/js-sdk/tests/stripAnsi.test.ts | 3 +++ packages/python-sdk/e2b/template/utils.py | 3 ++- .../shared/template/utils/test_strip_ansi_escape_codes.py | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/js-sdk/tests/stripAnsi.test.ts b/packages/js-sdk/tests/stripAnsi.test.ts index 72d04bb3cc..da993d1d42 100644 --- a/packages/js-sdk/tests/stripAnsi.test.ts +++ b/packages/js-sdk/tests/stripAnsi.test.ts @@ -83,3 +83,6 @@ test('strips APC', () => { test('strips only the intro of an unterminated DCS', () => { expect(stripAnsi('\x1bPno terminator here')).toBe('no terminator here') }) +test('leaves CSI with non-ASCII digits intact', () => { + expect(stripAnsi('\x1b[٣١mred\x1b[0m')).toBe('\x1b[٣١mred') +}) diff --git a/packages/python-sdk/e2b/template/utils.py b/packages/python-sdk/e2b/template/utils.py index a2fe70dfdb..1bb83f8733 100644 --- a/packages/python-sdk/e2b/template/utils.py +++ b/packages/python-sdk/e2b/template/utils.py @@ -324,7 +324,8 @@ def strip_ansi_escape_codes(text: str) -> str: csi = ( r"[\u001B\u009B][\[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]" ) - ansi_escape = re.compile(f"{strings}|{csi}") + # re.ASCII keeps \d to 0-9 like JS; [\s\S] still matches any char + ansi_escape = re.compile(f"{strings}|{csi}", re.ASCII) return ansi_escape.sub("", text) diff --git a/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py b/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py index 52299c9830..e18ba3ae0d 100644 --- a/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py +++ b/packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py @@ -84,3 +84,8 @@ def test_strips_apc(): def test_strips_unterminated_dcs_intro_only(): assert strip_ansi_escape_codes("\x1bPno terminator here") == "no terminator here" + + +def test_leaves_csi_with_non_ascii_digits_intact(): + # Python's \d would match Unicode digits without re.ASCII; JS \d never does + assert strip_ansi_escape_codes("\x1b[٣١mred\x1b[0m") == "\x1b[٣١mred"