Procházet zdrojové kódy

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 před 1 měsícem
rodič
revize
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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů