Преглед изворни кода

test(code-runtime-python): drive the inherited SIGXCPU path with a wrapper; fix the zh outer wire sentence

The review's two follow-ups on the inherited-SIGXCPU fix: (1) a discriminating
case — pythonBin points at a wrapper that ignores SIGXCPU before exec'ing
python3, so the child genuinely inherits the ignore; with cpuSeconds: 1 the
busy loop must end as timeout (the bootstrap reset restored SIG_DFL), and
reverting the reset leaves it running to the wall — verified red. (2) The zh
README's OUTER wire section now carries the truncation-exception sentence
(the previous commit had duplicated it in the inner section instead); the
duplicate is removed, and the settlement note registers the inherited-SIGXCPU
reset.
Chinesezjc пре 2 недеља
родитељ
комит
c4fe0320fb

+ 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: 318545fd769fc2b1e116ec3c679fd53ee1ff2311
-2026-07-31-code-runtime-python-settlement-fixes.zh.md: d24dc60a9a6f9699ac495389959430c83ad8fd1e
+2026-07-31-code-runtime-python-settlement-fixes.md: 33c0495fba322a9bc5545c1cdddd3df23d2f8df4
+2026-07-31-code-runtime-python-settlement-fixes.zh.md: 522f23ceba03440204e0f5b71334599434caa0b8

Разлика између датотеке није приказан због своје велике величине
+ 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


+ 1 - 1
packages/code-runtime/code-runtime-python/README.i18n.yaml

@@ -3,4 +3,4 @@
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-python/README.md
 README.md: 2e22a5122f891b8317e13ee395ce222805b01b29
-README.zh.md: 028d2f5f7b57f1ca4da46557ba66511992227379
+README.zh.md: a0372bbe1148ca259f1fc6849060d13fcec24947

+ 2 - 2
packages/code-runtime/code-runtime-python/README.zh.md

@@ -33,7 +33,7 @@ kind: "package-reference"
 
 ### wire
 
-帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:`boot-ack`、`call`、`log`、`done`。宿主 → 子进程:`boot`(首帧,携带全部上限与命名空间声明)、`run`(`boot-ack` 之后,只携带程序体)与每个 `call` 一个 `reply`。伪造帧可在 `done` 上同时携带 `value` 与 `error`,因此消费方必须先检查 `error`,在它存在时忽略 `value`。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧追加到同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行。
+帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:`boot-ack`、`call`、`log`、`done`。宿主 → 子进程:`boot`(首帧,携带全部上限与命名空间声明)、`run`(`boot-ack` 之后,只携带程序体)与每个 `call` 一个 `reply`。伪造帧可在 `done` 上同时携带 `value` 与 `error`,因此消费方必须先检查 `error`,在它存在时忽略 `value`。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧追加到同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。
 
 ### 可能出错的地方
 
@@ -55,7 +55,7 @@ kind: "package-reference"
 
 ### wire 契约
 
-帧为 `boot`/`run`(宿主 → 子进程)与 `boot-ack`/`call`/`log`/`done` 加每个 call 一个 `reply`(子进程 → 宿主)。`log` 帧的 `truncated` 标志标记的就是子进程账本自己的截断标记帧,因此宿主在与子进程相同的点停止捕获,而不是从自己的预算推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧合并进同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行(拆分计费算术在 fd-3 协议 Agent Note 的 wire-contract 段)。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。`done.error.kind` 为 `exception`、`invalid-output`、`output-limit` 之一;墙钟/CPU 预算、中止与基底死亡在宿主侧观察,不以帧形式携带。
+帧为 `boot`/`run`(宿主 → 子进程)与 `boot-ack`/`call`/`log`/`done` 加每个 call 一个 `reply`(子进程 → 宿主)。`log` 帧的 `truncated` 标志标记的就是子进程账本自己的截断标记帧,因此宿主在与子进程相同的点停止捕获,而不是从自己的预算推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧合并进同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行(拆分计费算术在 fd-3 协议 Agent Note 的 wire-contract 段)。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。`done.error.kind` 为 `exception`、`invalid-output`、`output-limit` 之一;墙钟/CPU 预算、中止与基底死亡在宿主侧观察,不以帧形式携带。
 
 ### 无损 JSON 跨越
 

+ 21 - 1
packages/code-runtime/code-runtime-python/tests/runtime.spec.ts

@@ -1,4 +1,4 @@
-import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs'
+import { existsSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs'
 import { mkdtemp, writeFile } from 'node:fs/promises'
 import { tmpdir } from 'node:os'
 import { basename, dirname, join } from 'node:path'
@@ -638,6 +638,26 @@ describe('PythonCodeRuntime — inherited resource limits', () => {
     expect(result.value).toBeUndefined()
   }, 20_000)
 
+  it('reports a timeout when the interpreter was started with SIGXCPU ignored (inherited state)', async () => {
+    // The child inherits the host's SIGXCPU disposition: a wrapper that
+    // ignores SIGXCPU before exec'ing python3 hands the child a soft
+    // RLIMIT_CPU that cannot stop it. The bootstrap resets SIGXCPU to SIG_DFL
+    // before model code runs, so a busy loop still ends as a timeout rather
+    // than running to the hard limit and being misclassified as worker-exit.
+    const wrapper = join(tmpdir(), `dsh-xcpu-ignore-${process.pid}.sh`)
+    writeFileSync(wrapper, '#!/bin/sh\ntrap "" XCPU\nexec python3 "$@"\n', { mode: 0o755 })
+    try {
+      const { runtime } = await setup({ maxWallMs: 30_000, cpuSeconds: 1, pythonBin: wrapper })
+      const result = await runtime.run({
+        program: ['while True: pass'].join('\n'),
+        bindings: [],
+      })
+      expect(result.error?.kind).toBe('timeout')
+    } finally {
+      rmSync(wrapper, { force: true })
+    }
+  }, 20_000)
+
   it('reports a timeout when a program traps AND masks SIGXCPU and returns past the soft limit', async () => {
     // The mask-only case exercises the unblock; the trap+mask combination is
     // the harder one: a program that installed a custom handler AND masked the

Неке датотеке нису приказане због велике количине промена