Repository navigation
fix(harness): decode a Python file before searching it when its codec can spell a name (TH-9490) - #276
Merged
khushalsonawat merged 1 commit intoNov 11, 2026
Conversation
… can spell a name A coding line can name a codec that spells ASCII letters in other bytes (raw_unicode_escape, utf-7, the iso2022 codecs, hz, mac-arabic and mac-farsi), so a file could import playwright or traceai_openai while its raw bytes never held the name. The browser and instrumentation scans searched those raw bytes, and the import walk decoded such files before ending their lines, or read unicode-escape as UTF-8. Every codec not proven safe to search as bytes is now decoded first, as Python reads source (lines ended, a final line end, NUL cut), under the scan's one 4 MiB decode total. The 81 safe codecs are proven by a sweep test. A decoded-first file passes the gate when a needle is in its text, or in its raw bytes when the text isn't ASCII, so no file base lets in is stopped. The walk reads the same way, with its own total, and follows the union of both readings for a unicode-escape file whose text holds a CR, charged once. Refs: TH-9490 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
This was referenced Nov 10, 2026
cdileep23
requested changes
Nov 11, 2026
khushalsonawat
merged commit Nov 11, 2026
73e9170
into
feat/TH-8804-source-subfolder
11 checks passed
khushalsonawat
added a commit
that referenced
this pull request
Nov 11, 2026
…total Brings in #269, #276 (through #283), #283 and #288. Three hunks against #288 (TH-9448) were resolved by hand, keeping both intents: - a skipped file logs its type once at WARNING, and its path and trace at DEBUG (#288); it is also counted as the scan's `skipped` cut, and that count is in the cut line, so #288's separate "skipped N files" line is not repeated (TH-9512); - #288's skip tests and its NUL-byte test now expect the cut line, which holds names and counts only. Tests on 3.12.3: test_agent_type_refusal 720, test_browser_scan_cut 59, test_contract_tools_subfolder 180, all passed. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
This was referenced Nov 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on
feat/TH-8804-source-subfolder(#225's branch, atcf3ebb44). The diff shows only TH-9490.Summary
The browser scan and the instrumentation scan look for imports by name, and the import walk decides which files outside the agent's folder both of them read. All three searched a Python file's raw bytes, or decoded it the wrong way, before looking for a name. A file whose coding line names a codec that spells ASCII letters in other bytes could therefore import
playwrightortraceai_openaiunseen. Python runs each of these as the import:raw_unicode_escape(playwright);utf-7(+AHA-laywright);iso2022_*codecs (playESC(Bwright);hz(play~{~}wright, orplay~then a line end);mac-arabicandmac-farsi(traceai0xDFopenai, where 0xDF decodes to_).After this PR, such a file is decoded first, as Python decodes it, and the name is looked for in what it decodes to.
Ticket: TH-9490. TH-9448 item 7 (
import \x70laywrightunderunicode-escape) was already fixed by #225. This PR adds a no-final-LF case to its test.How it works
Which codecs.
python_source._RAW_SAFE_CODECSlists the 81 codecs Python accepts on a coding line in which no byte form spells a name character, NUL, CR or LF except its own single byte. A test proves each one with a sweep of every byte form, and fails if a new Python adds a codec that isn't classified. Every other text codec is decoded first: today the 13 that can spell a name, plus 13 that Python refuses on a coding line.idna,punycode,undefinedand non-text codecs keep today's handling.How it decodes (
_decoded_first, as Python reads source):~before a CRLF joins the lines.A CR the decode makes is left in place, exactly as today's code hands text to the parser.
One total. Every decoded-first file is charged to the scan's existing 4 MiB
EscapeBudget, whichunicode-escapealready used. Past it, a file takes today's path. That caps the extra work at about 0.05 s per scan.The gate. A decoded-first file passes when a needle is in the decoded text, or in its raw bytes when that text isn't ASCII. Base gates on the raw bytes, so no file base lets in is stopped.
The import walk (
credentials.imported_by_agent) uses the same helper for.pyfiles, with its own total, and skips a file Python refuses.unicode-escapefile whose decoded text holds a CR is also read the way the walk read it before (as UTF-8, where\ris two characters), and the union of both readings' imports is followed. So the walk never reaches less than it did.Known misses, pinned by tests
_coding's cap, which stops a crafted-head stall. TH-9524, for TH-8804's owner.Review
Each round used two fresh Opus reviewers.
unicode-escapefile with an escaped\r. That's fixed by the union.unicode-escapeonly, and the single charge;Verification
ruff checkandruff format --checkon the new test file; no new findings in the edited files againstcf3ebb44;compileall.One test run, a file at a time:
test_python_source_codecs(new)test_agent_type_refusaltest_instrumentation_evidencetest_source_tool_evidencetest_contract_tools_subfoldertest_scan_walktest_hosted_authoring_entrypointtest_harness_credentialstest_bundle_author_v2test_harness_sandbox_servercf3ebb44(HarnessJobvalidation insandbox_server.py:541), so they're older than this change49 bugs planted one at a time; 46 fail a named test. Three survive:
test_a_reading_is_sized_with_its_line_ends_folded[decoded, utf8], and each case fails only under its own mutant.TypeScript: not touched.
Checklist