diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml
index 05ed50f637..34a16685a4 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.i18n.yaml
+++ b/.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: 4e76c78e964608822ca5bed68870ee3f1df38911
-2026-07-31-code-runtime-python-settlement-fixes.zh.md: 6f0bc792ddb19e66f4918c8d8499ddf2846fed58
+2026-07-31-code-runtime-python-settlement-fixes.md: 26a5947b6602d56dc291f2f2e692743522580b12
+2026-07-31-code-runtime-python-settlement-fixes.zh.md: f2e7955a40b00f5b08017f44d2cf0529b2b31bfb
diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md
index 4e76c78e96..26a5947b66 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md
+++ b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.md
@@ -70,7 +70,7 @@ Also in `src/index.ts`, `spawn` is called before the settlement Promise executor
### Interpreter selection and the child environment settle at load
-`pythonBin` resolves once at plugin load to an executable absolute path and is version-probed under the same scrubbed environment used for runs. The provider requires CPython 3.10 or newer and retains that exact path, so a later `PATH` or working-directory change cannot switch interpreters; an explicit path that is not an executable regular file, an unresolved basename, or an unsupported interpreter fails before `ctx.codeRuntime` registers. Each probe and run receives only `TMPDIR`: macOS system Python needs it to avoid emitting a startup warning into captured stderr, while credentials, `PATH`, `HOME`, and every other ambient host value remain unavailable to model code. If the validated executable disappears after activation, the ordinary spawn settlement still resolves `worker-exit`.
+`pythonBin` resolves once at plugin load to an executable absolute path and is version-probed under the same scrubbed environment used for runs. The provider requires CPython 3.10 or newer and retains that exact path, so a later `PATH` or working-directory change cannot switch interpreters; an explicit path that is not an executable regular file, an unresolved basename, or an unsupported interpreter fails before `ctx.codeRuntime` registers. The synchronous probe has a fixed five-second deadline and sends `SIGKILL` at that deadline, so a wrapper that ignores `SIGTERM` cannot block plugin load. Each probe and run receives only `TMPDIR`: macOS system Python needs it to avoid emitting a startup warning into captured stderr, while credentials, `PATH`, `HOME`, and every other ambient host value remain unavailable to model code. If the validated executable disappears after activation, the ordinary spawn settlement still resolves `worker-exit`.
### Stray pipe output is aggregated by line, not by transport chunk
diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md
index 6f0bc792dd..f2e7955a40 100644
--- a/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md
+++ b/.agents/notes/implemented/bug-fix/2026-07-31-code-runtime-python-settlement-fixes.zh.md
@@ -70,7 +70,7 @@ unknown-binding 回复用 `JSON.stringify` 对完整的限幅 target(`global`
### 解释器选择与子进程环境在加载期固定
-`pythonBin` 在插件加载期解析为一个可执行绝对路径,并在与运行时相同的受限环境中完成版本探测。提供方要求 CPython 3.10 或更高版本并保留该确切路径,因此后续 `PATH` 或工作目录变化不能切换解释器;不是可执行普通文件的显式路径、无法解析的裸名或不受支持的解释器都会在 `ctx.codeRuntime` 注册前失败。每次探测与运行只接收 `TMPDIR`:macOS 系统 Python 需要它来避免向被捕获的 stderr 发出启动警告,而凭证、`PATH`、`HOME` 与其他宿主环境值均不会进入模型代码。若已校验的可执行文件在激活后消失,普通 spawn 结算仍 resolve 为 `worker-exit`。
+`pythonBin` 在插件加载期解析为一个可执行绝对路径,并在与运行时相同的受限环境中完成版本探测。提供方要求 CPython 3.10 或更高版本并保留该确切路径,因此后续 `PATH` 或工作目录变化不能切换解释器;不是可执行普通文件的显式路径、无法解析的裸名或不受支持的解释器都会在 `ctx.codeRuntime` 注册前失败。同步探测有固定的五秒期限,并在期限到达时发送 `SIGKILL`,因此忽略 `SIGTERM` 的包装脚本不能阻塞插件加载。每次探测与运行只接收 `TMPDIR`:macOS 系统 Python 需要它来避免向被捕获的 stderr 发出启动警告,而凭证、`PATH`、`HOME` 与其他宿主环境值均不会进入模型代码。若已校验的可执行文件在激活后消失,普通 spawn 结算仍 resolve 为 `worker-exit`。
### Stray pipe output is aggregated by line, not by transport chunk
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index de2624fd29..60ae2fd7ce 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.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 docs/config-catalog.md
-config-catalog.md: 45a246f9baf126333aabe6fd158ede0100ee1508
-config-catalog.zh.md: 5011b6e1f7be6d8e90c6eb968c993599e7eed28e
+config-catalog.md: 0e936de187dfe17c763f2b98fc88504ef857fb8f
+config-catalog.zh.md: a316fad83c62d70cfca76275ff35fb33b8f13a96
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 45a246f9ba..0e936de187 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -588,13 +588,14 @@ export interface Config {
graceMs?: number
/**
* Absolute path, relative path, or basename of a CPython 3.10+ interpreter.
- * Resolved and validated once at plugin load; a basename searches `PATH`.
+ * Resolved and validated once at plugin load under a five-second force-kill
+ * deadline; a basename searches `PATH`.
*/
pythonBin?: string
}
```
-Source: [`packages/experimental/code-runtime-python/src/index.ts:44`](../packages/experimental/code-runtime-python/src/index.ts)
+Source: [`packages/experimental/code-runtime-python/src/index.ts:43`](../packages/experimental/code-runtime-python/src/index.ts)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 5011b6e1f7..a316fad83c 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -590,13 +590,14 @@ export interface Config {
graceMs?: number
/**
* Absolute path, relative path, or basename of a CPython 3.10+ interpreter.
- * Resolved and validated once at plugin load; a basename searches `PATH`.
+ * Resolved and validated once at plugin load under a five-second force-kill
+ * deadline; a basename searches `PATH`.
*/
pythonBin?: string
}
```
-来源:[`packages/experimental/code-runtime-python/src/index.ts:44`](../packages/experimental/code-runtime-python/src/index.ts)
+来源:[`packages/experimental/code-runtime-python/src/index.ts:43`](../packages/experimental/code-runtime-python/src/index.ts)
diff --git a/packages/experimental/code-runtime-python/README.i18n.yaml b/packages/experimental/code-runtime-python/README.i18n.yaml
index d5ca0fced2..7308573210 100644
--- a/packages/experimental/code-runtime-python/README.i18n.yaml
+++ b/packages/experimental/code-runtime-python/README.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 packages/experimental/code-runtime-python/README.md
-README.md: 8b596ec5e8bcb7a0d458fe11e61c3742b6efc823
-README.zh.md: a0035beec49a531d7ff37363921ecefa869877a6
+README.md: 117daeac38c329521e5334b41106b71c629a0efc
+README.zh.md: 0727c705a8eb024f068804c0a1af6e81c2e02d6b
diff --git a/packages/experimental/code-runtime-python/README.md b/packages/experimental/code-runtime-python/README.md
index 8b596ec5e8..117daeac38 100644
--- a/packages/experimental/code-runtime-python/README.md
+++ b/packages/experimental/code-runtime-python/README.md
@@ -29,7 +29,7 @@ Choose this private experimental package only in an explicit source-checkout com
### What you get
-The package's default export is the `PythonCodeRuntime` plugin. Its public surface also re-exports the host-side protocol vocabulary: `validateChildFrame` (rebuilds every inbound frame), the lossless-JSON codec and meters (`encodeJsonPlain`, `checkDoneValue`, `hasUnsafeIntegerToken`, `hasNonLosslessNumber`), `logTruncationMarker` (the shared truncation-marker text), plus `resolvePythonBin` (interpreter lookup against the current `PATH`), `readProcessStart` (process-start statistics for tests), and `detachResidual` (a test seam for the settled run's resource cleanup). Every cap is a validated `Config` field with a default: `cpuSeconds` (60), `maxWallMs` (600000), `addressSpaceMb` (512, not applied on Darwin), `maxLogBytes` (65536), `maxValueBytes` (32768), `graceMs` (3000), and `pythonBin` (`python3`, resolved, executable-checked, version-probed, and frozen at load). Each child receives only `TMPDIR`; ambient credentials, `PATH`, `HOME`, and other host state stay unavailable.
+The package's default export is the `PythonCodeRuntime` plugin. Its public surface also re-exports the host-side protocol vocabulary: `validateChildFrame` (rebuilds every inbound frame), the lossless-JSON codec and meters (`encodeJsonPlain`, `checkDoneValue`, `hasUnsafeIntegerToken`, `hasNonLosslessNumber`), `logTruncationMarker` (the shared truncation-marker text), plus `resolvePythonBin` (interpreter lookup against the current `PATH`), `readProcessStart` (process-start statistics for tests), and `detachResidual` (a test seam for the settled run's resource cleanup). Every cap is a validated `Config` field with a default: `cpuSeconds` (60), `maxWallMs` (600000), `addressSpaceMb` (512, not applied on Darwin), `maxLogBytes` (65536), `maxValueBytes` (32768), `graceMs` (3000), and `pythonBin` (`python3`, resolved, executable-checked, version-probed under a five-second force-kill deadline, and frozen at load). Each child receives only `TMPDIR`; ambient credentials, `PATH`, `HOME`, and other host state stay unavailable.
### The wire
diff --git a/packages/experimental/code-runtime-python/README.zh.md b/packages/experimental/code-runtime-python/README.zh.md
index a0035beec4..0727c705a8 100644
--- a/packages/experimental/code-runtime-python/README.zh.md
+++ b/packages/experimental/code-runtime-python/README.zh.md
@@ -29,7 +29,7 @@ kind: "package-reference"
### 你得到什么
-包的默认导出是 `PythonCodeRuntime` 插件。其公开面还重新导出宿主侧协议词汇:`validateChildFrame`(重建每条入站帧)、无损 JSON codec 与计量器(`encodeJsonPlain`、`checkDoneValue`、`hasUnsafeIntegerToken`、`hasNonLosslessNumber`)、`logTruncationMarker`(共享截断标记文本),以及 `resolvePythonBin`(对照当前 `PATH` 的解释器查找)、`readProcessStart`(供测试用的进程启动统计)和 `detachResidual`(已结算运行的资源清理测试 seam)。每个上限都是带默认值并经校验的 `Config` 字段:`cpuSeconds`(60)、`maxWallMs`(600000)、`addressSpaceMb`(512,Darwin 上不生效)、`maxLogBytes`(65536)、`maxValueBytes`(32768)、`graceMs`(3000)与 `pythonBin`(`python3`,在加载期解析、检查可执行性、探测版本并固定)。每个子进程只接收 `TMPDIR`;环境中的凭证、`PATH`、`HOME` 与其他宿主状态均不可见。
+包的默认导出是 `PythonCodeRuntime` 插件。其公开面还重新导出宿主侧协议词汇:`validateChildFrame`(重建每条入站帧)、无损 JSON codec 与计量器(`encodeJsonPlain`、`checkDoneValue`、`hasUnsafeIntegerToken`、`hasNonLosslessNumber`)、`logTruncationMarker`(共享截断标记文本),以及 `resolvePythonBin`(对照当前 `PATH` 的解释器查找)、`readProcessStart`(供测试用的进程启动统计)和 `detachResidual`(已结算运行的资源清理测试 seam)。每个上限都是带默认值并经校验的 `Config` 字段:`cpuSeconds`(60)、`maxWallMs`(600000)、`addressSpaceMb`(512,Darwin 上不生效)、`maxLogBytes`(65536)、`maxValueBytes`(32768)、`graceMs`(3000)与 `pythonBin`(`python3`,在加载期解析、检查可执行性,在五秒强制终止期限内探测版本并固定)。每个子进程只接收 `TMPDIR`;环境中的凭证、`PATH`、`HOME` 与其他宿主状态均不可见。
### wire
diff --git a/packages/experimental/code-runtime-python/src/index.ts b/packages/experimental/code-runtime-python/src/index.ts
index acf0ea6aae..9826b6b1d3 100644
--- a/packages/experimental/code-runtime-python/src/index.ts
+++ b/packages/experimental/code-runtime-python/src/index.ts
@@ -85,7 +85,8 @@ export interface Config {
graceMs?: number
/**
* Absolute path, relative path, or basename of a CPython 3.10+ interpreter.
- * Resolved and validated once at plugin load; a basename searches `PATH`.
+ * Resolved and validated once at plugin load under a five-second force-kill
+ * deadline; a basename searches `PATH`.
*/
pythonBin?: string
}
@@ -413,6 +414,8 @@ export function resolvePythonBin(bin: string): string | undefined {
accessSync(candidate, fsConstants.X_OK)
return statSync(candidate).isFile() ? candidate : undefined
} catch {
+ // Missing, inaccessible, and non-stat-able candidates are ordinary
+ // lookup misses; the constructor reports the final load error.
return undefined
}
}
@@ -457,6 +460,9 @@ function validatePythonBin(bin: string): void {
encoding: 'utf8',
env: pythonEnvironment(),
timeout: PYTHON_PROBE_TIMEOUT_MS,
+ // The configured executable is outside our control. Force-kill it at the
+ // deadline so a wrapper that ignores SIGTERM cannot block plugin load.
+ killSignal: 'SIGKILL',
maxBuffer: 1_024,
}).trim()
} catch (error: unknown) {
diff --git a/packages/experimental/code-runtime-python/tests/boot-write-failure.spec.ts b/packages/experimental/code-runtime-python/tests/boot-write-failure.spec.ts
index f5499b9a37..759d0a440d 100644
--- a/packages/experimental/code-runtime-python/tests/boot-write-failure.spec.ts
+++ b/packages/experimental/code-runtime-python/tests/boot-write-failure.spec.ts
@@ -13,11 +13,12 @@ import { Context } from 'cordis'
* which is exactly the branch that regressed. The mock is confined to this file
* so the real-subprocess suite in runtime.spec.ts is untouched.
*/
-const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }))
-vi.mock('node:child_process', async importOriginal => ({
- ...(await importOriginal()),
- spawn: spawnMock,
-}))
+const { execFileSyncMock, spawnMock } = vi.hoisted(() => ({ execFileSyncMock: vi.fn(), spawnMock: vi.fn() }))
+vi.mock('node:child_process', async (importOriginal) => {
+ const original = await importOriginal()
+ execFileSyncMock.mockImplementation(original.execFileSync)
+ return { ...original, execFileSync: execFileSyncMock, spawn: spawnMock }
+})
const { PythonCodeRuntime } = await import('../src/index.ts')
@@ -44,6 +45,7 @@ function fakeChildWithThrowingFd3(): EventEmitter {
}
afterEach(() => {
+ execFileSyncMock.mockClear()
spawnMock.mockReset()
})
@@ -127,6 +129,18 @@ function fakeChildBackpressuredThenDestroyed(): { child: EventEmitter; proto: Pa
}
describe('PythonCodeRuntime — boot-write failure', () => {
+ it('force-kills a version probe that exceeds its load-time deadline', async () => {
+ const ctx = new Context()
+ const fiber = await ctx.plugin(PythonCodeRuntime)
+
+ expect(execFileSyncMock).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.arrayContaining(['-I', '-c']),
+ expect.objectContaining({ timeout: 5_000, killSignal: 'SIGKILL' }),
+ )
+ await fiber.dispose()
+ })
+
it('resolves a worker-exit when the fd-3 boot write throws (no TDZ ReferenceError)', async () => {
// Before the fix, the boot-write block ran BEFORE `wallTimer`, `onAbort`,
// and `live` were initialized, so its `finish()` (which clears `wallTimer`,