فهرست منبع

fix(code-runtime-python): catch the model exception with a pre-program local exception class

The _run outer try/except used the module-global BaseException, which the
program (running as __main__) can rebind: __main__.BaseException = RuntimeError
made the except resolve to RuntimeError, so a subsequent ValueError escaped _run
with no done frame and misreported the run as worker-exit. Bind BaseException
into a _run local before the program runs so the catch is immune; a regression
test rebinds BaseException and raises, asserting an exception, not a worker-exit.

Also correct the NUL-escape comment text: the JSON escape-result side is \^@ (6
bytes, the valid JSON NUL escape), not \x00, so the 6x-budget arithmetic in the
comments is self-consistent. Register the BaseException-rebind case in the
settlement note Testing (en + zh) and re-record the pairing.
Chinesezjc 1 ماه پیش
والد
کامیت
0102cd95bf

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md
-2026-07-31-code-runtime-python-settlement-fixes.md: 9624056026f0cdc972493220f85c42d6c65a206b
-2026-07-31-code-runtime-python-settlement-fixes.zh.md: b399a52047c7a64ecf98f2c8717594cfbb7cba40
+2026-07-31-code-runtime-python-settlement-fixes.md: 0bbf5280afef5071a78c51beb5ea48bcf7223a63
+2026-07-31-code-runtime-python-settlement-fixes.zh.md: befa577b5f1e8106de92b28bb9366f41c9773aac

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md


+ 10 - 1
packages/code-runtime/code-runtime-python/py/bootstrap.py

@@ -998,6 +998,15 @@ async def _run(channel: ProtocolChannel) -> None:
     _os_write_local = _os_write
     _memoryview_local = _memoryview
     _fallback_frame_local = _FALLBACK_DONE_FRAME
+    # The exception class the outer try/except catches is bound into a LOCAL
+    # here, before the program runs. This bootstrap is `__main__`, so
+    # `__main__.BaseException = RuntimeError` would otherwise rebind the module
+    # global `BaseException` the `except BaseException` clause resolves at
+    # runtime, and a subsequent `ValueError` would then not match the clause —
+    # escaping `_run` with no `done` frame and misreporting the run as a
+    # `worker-exit`. Binding the class into a local makes the catch immune to a
+    # one-line rebind.
+    _BaseException = BaseException
 
     def send_done(payload: dict[str, Any] | str) -> None:
         try:
@@ -1061,7 +1070,7 @@ async def _run(channel: ProtocolChannel) -> None:
         flush_out()
         flush_err()
         done = _done_with_value(value, max_value_bytes)
-    except BaseException as exc:  # noqa: BLE001 -- report every failure to host
+    except _BaseException as exc:  # noqa: BLE001 -- report every failure to host; `_BaseException` is a pre-program local, not a rebindable module global
         done = {
             "type": "done",
             "error": {

+ 27 - 4
packages/code-runtime/code-runtime-python/tests/runtime.spec.ts

@@ -848,7 +848,7 @@ describe('PythonCodeRuntime — programs and bindings', () => {
 
   it('bounds a control-char-dense native residual by serialized cost, not raw length', async () => {
     // A newline-free NUL flood passes the cheap `length + 3` lower bound at a
-    // raw length well under the budget, but each NUL serializes to `\x00` (6
+    // raw length well under the budget, but each NUL serializes to `\^@` (6
     // bytes), so the true JSON cost is ~6x. The ledger must charge that
     // serialized cost — and `jsonStringCostUpTo` must measure it WITHOUT
     // allocating the escaped copy, so a near-budget line under a large
@@ -1513,6 +1513,29 @@ describe('PythonCodeRuntime — programs and bindings', () => {
     expect(result.error?.kind).not.toBe('worker-exit')
   }, 15_000)
 
+  it('still reports a model exception when the program rebinds BaseException', async () => {
+    // `_run`'s outer try/except catches the program's failure and builds a
+    // `done` frame. The clause previously used the module-global `BaseException`,
+    // which the program (running as `__main__`) can rebind: `__main__.BaseException
+    // = RuntimeError` makes the `except BaseException` resolve to `RuntimeError`,
+    // so a subsequent `ValueError` does not match and escapes `_run` with no
+    // `done` frame — misreporting the run as a `worker-exit`. The exception class
+    // is now bound into a `_run` LOCAL before the program runs, so the rebind
+    // cannot change which class the clause catches; the run must still report an
+    // `exception`, not a `worker-exit`.
+    const { runtime } = await setup({ maxWallMs: 10_000 })
+    const result = await runtime.run({
+      program: [
+        'import __main__',
+        '__main__.BaseException = RuntimeError',
+        'raise ValueError("real failure")',
+      ].join('\n'),
+      bindings: [],
+    })
+    expect(result.error?.kind).toBe('exception')
+    expect(result.error?.kind).not.toBe('worker-exit')
+  }, 15_000)
+
   it('bounds an over-cap exception-group nesting on the copy', async () => {
     // Exception groups link through `exceptions`, not the cause/context
     // dunders, so the cap has to count that edge too — otherwise a deeply
@@ -3269,7 +3292,7 @@ describe('PythonCodeRuntime — hostile peer', () => {
   }, 20_000)
 
   it('rejects a control-heavy oversized completion on its length, not its escaped copy', async () => {
-    // Every "\x00" escapes to the six bytes "\x00", so the escaped form of a
+    // Every "\x00" escapes to the six bytes "\^@", so the escaped form of a
     // 40 MB string is ~240 MB. The walk must refuse on the cheap
     // `len(current) + 2` lower bound; the 384 MiB address space holds the raw
     // string but not its escaped expansion, so a pre-escape check dies on
@@ -3523,7 +3546,7 @@ describe('PythonCodeRuntime — hostile peer', () => {
 
   it('caps a control-heavy exception diagnostic by its serialized cost, not raw bytes', async () => {
     // The diagnostic crosses fd 3 inside a JSON frame where a control character
-    // escapes sixfold (a NUL is one raw byte, six as `\x00`). Capping by raw
+    // escapes sixfold (a NUL is one raw byte, six as `\^@`). Capping by raw
     // UTF-8 length would let a NUL-heavy message near maxValueBytes serialize to
     // ~6x that and breach the frame ceiling — the silent worker-exit inversion
     // the load-time cap check exists to prevent. The child meters the diagnostic
@@ -4157,7 +4180,7 @@ describe('PythonCodeRuntime — hostile peer', () => {
   it('drops a forged oversized log frame on its code-unit lower bound, before escaping it', async () => {
     // A forged `log` frame carrying a control-heavy string sits below the
     // 256 MiB fd-3 frame ceiling but escapes several-fold: 24 MiB of NULs
-    // becomes ~144 MiB of `\x00`. Charging it required building that escaped
+    // becomes ~144 MiB of `\^@`. Charging it required building that escaped
     // copy first, so a 32-byte maxLogBytes could still force a
     // hundreds-of-megabytes host allocation. The cheap `length + 3` lower bound
     // truncates it instead. The host's own heap is what is under test, so keep

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است