From b8fac4b9726c4eb9908e4e2dcfa5dac1fb3e5348 Mon Sep 17 00:00:00 2001 From: skulitom Date: Thu, 6 Aug 2026 01:15:12 +0100 Subject: [PATCH] fix(coding-agent): resolve kernel venv interpreter for Windows layout bootstrapVenv and ensureKernelPythonUncached built the interpreter path as /bin/python, which only exists on POSIX. On Windows the interpreter is at /Scripts/python.exe, so uv pip install --python got a path that does not exist and exited 2, leaving the kernel unusable. The same path feeds kernelReady, so on Windows it was always false: every launch fell through to bootstrapVenv, which runs uv venv --seed and recreates the venv. A manually provisioned venv was therefore discarded on the next run. Add venvPython to pick the layout from process.platform, matching the existing win32 branch in findExecutable, and use it at both sites. Update the two integration tests that hardcoded the POSIX candidate path. Fixes #660 Co-Authored-By: Claude Opus 5 --- packages/coding-agent/docs/rlm-runtime.md | 2 +- .../coding-agent/src/core/kernel/bootstrap.ts | 8 ++++++-- .../coding-agent/test/ipython-bootstrap.test.ts | 3 ++- .../coding-agent/test/kernel-bootstrap.test.ts | 16 ++++++++++++++++ .../test/kernel-state-roundtrip.test.ts | 3 ++- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/docs/rlm-runtime.md b/packages/coding-agent/docs/rlm-runtime.md index 4de095cf01..a3ceaf35dd 100644 --- a/packages/coding-agent/docs/rlm-runtime.md +++ b/packages/coding-agent/docs/rlm-runtime.md @@ -76,7 +76,7 @@ The Python side does not call providers or implement an agent loop. The kernel is created lazily on first IPython use. Python resolution is: 1. `PRIME_AGENT_KERNEL_PYTHON`, when it can import `ipykernel`; -2. `~/.prime/agent/kernel-venv/bin/python`, bootstrapped with `uv`; or +2. `~/.prime/agent/kernel-venv/bin/python` (`Scripts\python.exe` on Windows), bootstrapped with `uv`; or 3. the XDG data location when `~/.prime` is not writable. The managed environment includes Python 3.11, `ipykernel`, and `prime-agent-runtime`. A bootstrap marker detects stale environments. diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 9b12b4b413..79ffc0e250 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -119,6 +119,10 @@ function expandHome(filePath: string): string { return filePath; } +export function venvPython(venv: string): string { + return process.platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); +} + function fileContentHash(filePath: string): string { try { return `sha256:${createHash("sha256").update(readFileSync(filePath)).digest("hex")}`; @@ -725,7 +729,7 @@ async function bootstrapVenv( ): Promise { await mkdir(path.dirname(venv), { recursive: true }); const uv = await ensureUv(options); - const python = path.join(venv, "bin", "python"); + const python = venvPython(venv); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity(); @@ -886,7 +890,7 @@ async function ensureKernelPythonUncached( } const venv = await resolveWritableKernelVenvDir(); - const python = path.join(venv, "bin", "python"); + const python = venvPython(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; diff --git a/packages/coding-agent/test/ipython-bootstrap.test.ts b/packages/coding-agent/test/ipython-bootstrap.test.ts index 8aba99b463..fa5c23ba20 100644 --- a/packages/coding-agent/test/ipython-bootstrap.test.ts +++ b/packages/coding-agent/test/ipython-bootstrap.test.ts @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, describe, expect, it } from "vitest"; +import { venvPython } from "../src/core/kernel/bootstrap.js"; import { KernelManager } from "../src/core/kernel/index.js"; import { buildRlmBootstrapCode } from "../src/core/tools/ipython.js"; @@ -44,7 +45,7 @@ describe("IPython RLM bootstrap", () => { function resolveKernelPython(): string | null { const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + venvPython(join(homedir(), ".prime", "agent", "kernel-venv")), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue; diff --git a/packages/coding-agent/test/kernel-bootstrap.test.ts b/packages/coding-agent/test/kernel-bootstrap.test.ts index eeb2fc0d15..bed16ba120 100644 --- a/packages/coding-agent/test/kernel-bootstrap.test.ts +++ b/packages/coding-agent/test/kernel-bootstrap.test.ts @@ -10,6 +10,7 @@ import { getKernelVenvDir, type KernelPythonSkill, resolveRuntimeIdentity, + venvPython, } from "../src/core/kernel/bootstrap.js"; let tempDir = ""; @@ -174,6 +175,21 @@ describe("kernel bootstrap", () => { expect(getKernelVenvDir()).toBe(venv); }); + it("resolves the venv interpreter using the host platform layout", () => { + const originalPlatform = process.platform; + const setPlatform = (value: NodeJS.Platform) => + Object.defineProperty(process, "platform", { value, configurable: true }); + try { + setPlatform("win32"); + expect(venvPython("C:\\venv")).toBe(join("C:\\venv", "Scripts", "python.exe")); + + setPlatform("linux"); + expect(venvPython("/venv")).toBe(join("/venv", "bin", "python")); + } finally { + setPlatform(originalPlatform); + } + }); + it("bootstraps a missing venv with uv, ipykernel, prime-agent-runtime, and default extra packages", async () => { const logPath = installFakeUv(); const venv = join(tempDir, "kernel-venv"); diff --git a/packages/coding-agent/test/kernel-state-roundtrip.test.ts b/packages/coding-agent/test/kernel-state-roundtrip.test.ts index e79d88d927..4b2b23522d 100644 --- a/packages/coding-agent/test/kernel-state-roundtrip.test.ts +++ b/packages/coding-agent/test/kernel-state-roundtrip.test.ts @@ -3,13 +3,14 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { venvPython } from "../src/core/kernel/bootstrap.js"; import { KernelManager } from "../src/core/kernel/index.js"; /** Find a python that can launch an ipykernel and has dill, or null to skip. */ function resolveKernelPython(): string | null { const candidates = [ process.env.PRIME_AGENT_KERNEL_PYTHON, - join(homedir(), ".prime", "agent", "kernel-venv", "bin", "python"), + venvPython(join(homedir(), ".prime", "agent", "kernel-venv")), ].filter((p): p is string => Boolean(p)); for (const python of candidates) { if (!existsSync(python)) continue;