Skip to content

fix(python-sdk): port current JS stripAnsi regex to strip_ansi_escape_codes#1545

Open
mishushakov wants to merge 2 commits into
mainfrom
strip-ansi-python-sdk
Open

fix(python-sdk): port current JS stripAnsi regex to strip_ansi_escape_codes#1545
mishushakov wants to merge 2 commits into
mainfrom
strip-ansi-python-sdk

Conversation

@mishushakov

@mishushakov mishushakov commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

The Python SDK's strip_ansi_escape_codes (used to clean template build log messages) still used the old ansi-regex pattern, while the JS SDK's stripAnsi was rewritten in #895. This ports the current JS regex to Python so both SDKs clean logs identically: OSC sequences (hyperlinks, window titles) are matched non-greedily up to the first string terminator — including content spanning newlines — and CSI sequences are stripped without requiring a terminator.

Following review feedback, both implementations now also strip the remaining ECMA-48 string controls — DCS (Sixel, tmux passthrough), SOS, PM, and APC — through their string terminator, so control payloads don't leak into cleaned logs. This goes beyond upstream chalk/ansi-regex, click, and Rich, none of which fully strip DCS payloads, and restores what the old Python pattern handled.

Also mirrors the Python test suite into the JS SDK (which previously had no stripAnsi tests) — 20 identical cases per side — and verified byte-for-byte identical output between the two implementations on all of them. Includes a patch changeset for e2b and @e2b/python-sdk.

Example

Log messages that previously leaked OSC or DCS sequences into template build output are now cleaned:

from e2b.template.utils import strip_ansi_escape_codes

strip_ansi_escape_codes("\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07")  # "E2B"
strip_ansi_escape_codes("\x1b]0;title\nstill title\x07done")           # "done"
strip_ansi_escape_codes("\x1b[38:2::255:0:0mRED\x1b[0m")               # "RED"
strip_ansi_escape_codes("\x1bPq#0;2;0;0;0~~@@\x1b\\image")             # "image" (Sixel DCS)

🤖 Generated with Claude Code

…_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 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9bce30f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Patch
@e2b/python-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Localized change to log sanitization helpers with expanded tests; no auth, data, or API surface changes.

Overview
Template build log cleaning was inconsistent: Python still used an older strip_ansi_escape_codes pattern while JS had a newer stripAnsi. Python now uses the same regex shape as JS (non-greedy string controls through the first terminator, CSI without requiring a terminator). JS stripAnsi was broadened from OSC-only to also remove DCS/Sixel, SOS, PM, and APC payloads that could leak into cleaned output. Shared vitest/pytest cases were added so both implementations stay aligned. Patch changeset for e2b and @e2b/python-sdk.

Reviewed by Cursor Bugbot for commit 9bce30f. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from a7ff0fe. Download artifacts from this workflow run.

JS SDK (e2b@2.33.1-strip-ansi-python-sdk.0):

npm install ./e2b-2.33.1-strip-ansi-python-sdk.0.tgz

CLI (@e2b/cli@2.13.3-strip-ansi-python-sdk.0):

npm install ./e2b-cli-2.13.3-strip-ansi-python-sdk.0.tgz

Python SDK (e2b==2.32.0+strip.ansi.python.sdk):

pip install ./e2b-2.32.0+strip.ansi.python.sdk-py3-none-any.whl

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f282d25cd8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

]
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})"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve stripping for DCS string controls

When Python build output contains ST-terminated ANSI string controls other than OSC, such as DCS/Sixel or tmux passthrough (ESC P ... ESC\\), this new OSC-only branch no longer removes the whole sequence: strip_ansi_escape_codes("\x1bPabc\x1b\\done") now leaves abc\x1b\\done, while the previous regex returned done. Since this helper sanitizes template log messages, those control payloads can leak into Python logs; keep handling the prior ST-terminated string-control form or add an explicit DCS branch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 9bce30f. The string-control branch now covers all ECMA-48 string controls (OSC, DCS, SOS, PM, APC — ESC ]/P/X/^/_ ... ST) in both the JS and Python implementations, so strip_ansi_escape_codes("\x1bPabc\x1b\\done") returns "done" again. Added DCS/Sixel, SOS, PM, APC, and unterminated-DCS test cases to both suites (20 mirrored cases per side, byte-for-byte identical output verified).

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 <noreply@anthropic.com>
Comment on lines 316 to 329
"""
# 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)


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Python's \d in the ported CSI regex matches any Unicode Nd decimal digit (e.g. Arabic-Indic ٣١ or fullwidth digits), not just ASCII 0-9 like JS's \d always does — so a CSI escape containing a non-ASCII digit is fully stripped in Python but left intact (except the trailing ASCII reset) in JS, breaking the byte-for-byte parity this PR claims. Fix by compiling the pattern with re.ASCII (verified this doesn't affect the OSC [\s\S] matching) or replacing \d with [0-9] in the csi pattern.

Extended reasoning...

The bug: In packages/python-sdk/e2b/template/utils.py (lines 316-329), the new csi pattern uses \d{1,4} for CSI parameter digits and [\dA-PR-TZcf-nq-uy=><~] for the final byte class. In Python, re's \d matches any Unicode category-Nd decimal digit by default (str patterns are Unicode in Python 3) — this includes things like Arabic-Indic digits (٣١, U+0663/U+0661) and fullwidth digits (11, U+FF11). JS's \d in the mirrored ansiRegex (packages/js-sdk/src/utils.ts lines 88-99) is always ASCII-only ([0-9]), regardless of flags, since JS regex is byte/UTF-16-unit oriented for \d without a special mode extending it to non-ASCII digits.\n\nWhere it triggers: Any string cleaned by strip_ansi_escape_codes that contains a CSI escape (ESC [ ... ) whose parameter bytes include a non-ASCII Unicode decimal digit. The regex still matches the whole CSI sequence as if it were digits 0-9, so Python strips it entirely, while the JS implementation the PR is explicitly trying to mirror leaves it un-matched (only unrelated valid ASCII sequences elsewhere in the string get stripped).\n\nWhy nothing currently prevents it: The PR's own test suites (14 cases, mirrored in both SDKs) only use ASCII digit parameters (e.g. 31, 1;31;42, 38;5;82), so this divergence isn't exercised by any test added here, despite the PR description explicitly claiming byte-for-byte identical output between the two implementations on all cases.\n\nStep-by-step proof (verified empirically):\n1. Construct s = ESC + "[" + "٣١" (Arabic-Indic 3,1) + "mred" + ESC + "[0m".\n2. Run it through the exact ported Python pattern (no flags): ansi_escape.sub("", s)'red' — the whole leading CSI escape (including the non-ASCII digits) is stripped, along with the trailing reset.\n3. Run the equivalent through the exact JS ansiRegex-derived pattern on the identical string: only the trailing ASCII ESC[0m reset is stripped; the leading escape survives as '\x1b[٣١mred'.\n4. Recompiling the Python pattern with re.compile(pattern, re.ASCII) reproduces the JS behavior exactly ('\x1b[٣١mred'), and re-running the OSC newline-spanning test case (ESC]0;line1\nline2\x07after) with the same re.ASCII-compiled pattern still correctly yields 'after', confirming the fix doesn't regress the OSC [\s\S] matching (which relies on \S/\s, not \d, so it's unaffected).\n\nImpact: This is a genuine, reproducible divergence from the PR's stated goal (byte-for-byte JS/Python parity for cleaning template build logs), and it lands directly on the exact lines this PR modifies. That said, valid ANSI/ECMA-48 CSI sequences restrict parameter bytes to ASCII 0x30-0x3F, so no legitimate terminal or build tool emits non-ASCII digits inside a real CSI sequence — the divergence only shows up on malformed or adversarial escape sequences, and the only consequence is a cosmetic difference in sanitized log display text (no crash, data loss, or security impact).\n\nFix: Either compile the combined pattern with re.ASCII, or replace \d with [0-9] explicitly in the csi pattern (and note the final-byte character class [\dA-PR-TZcf-nq-uy=><~] would also need \d[0-9] for full parity, or rely on re.ASCII to cover both in one place).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant