Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Changed helper provisioning to download pinned fd, ripgrep, and uv releases, verify their SHA-256 and archive contents before installing into `~/.prime/agent/bin`, and clean up partial downloads; uv is no longer installed by piping a remote script into `sh`.
2 changes: 2 additions & 0 deletions packages/coding-agent/docs/rlm-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ The kernel is created lazily on first Python REPL use. Python resolution is:

The managed environment includes Python 3.11, `prime-agent-runtime`, `dill`, and the default Python packages. A bootstrap marker detects stale environments.

When no `uv` is found on `PATH`, in `~/.prime/agent/bin`, or in `~/.local/bin`, the bootstrap offers to install one. Set `PRIME_AGENT_INSTALL_UV=1` to accept without a prompt (the installer does this) or `PRIME_AGENT_INSTALL_UV=0` to refuse. The install downloads the pinned `uv` release archive from GitHub, verifies it against the SHA-256 bundled in `src/utils/helper-tool-releases.ts`, checks that the extracted binary runs, and only then places it in `~/.prime/agent/bin/uv`; no remote script is executed. The optional `fd` and `rg` search helpers are provisioned into the same directory the same way. Pinned versions are bumped with `scripts/pin-helper-tools.ts`.

Startup spawns `python -m rlm.repl` and exchanges newline-delimited JSON over stdio: the runtime announces itself with a single `ready` event, then requests and events flow one JSON object per line (see `prime-agent-runtime/src/rlm/repl.md`).

The manager owns the child process and a bounded stderr tail. Shutdown sends a `shutdown` request, waits for the process to exit, and terminates it as a fallback. Persistent sessions may snapshot the kernel namespace into their session artifact directory for revival.
Expand Down
59 changes: 59 additions & 0 deletions packages/coding-agent/scripts/pin-helper-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Recompute the pinned SHA-256 table for a helper tool release.
//
// Usage (from packages/coding-agent): npx tsx scripts/pin-helper-tools.ts <fd|rg|uv> <version>
//
// Downloads every supported asset of that release from GitHub, prints its SHA-256, and
// cross-checks the upstream `<asset>.sha256` file when the project publishes one. Paste
// the printed table into src/utils/helper-tool-releases.ts together with the new version.
import { createHash } from "node:crypto";
import { HELPER_TOOL_RELEASES, type HelperToolId } from "../src/utils/helper-tool-releases.js";

const SUPPORTED_TARGETS: Array<[platform: string, architecture: string]> = [
["darwin", "arm64"],
["darwin", "x64"],
["linux", "arm64"],
["linux", "x64"],
["win32", "arm64"],
["win32", "x64"],
];

async function fetchBytes(url: string): Promise<Uint8Array> {
const response = await fetch(url);
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
return new Uint8Array(await response.arrayBuffer());
}

async function main(): Promise<void> {
const [tool, version] = process.argv.slice(2);
if (!tool || !version || !(tool in HELPER_TOOL_RELEASES)) {
console.error("usage: npx tsx scripts/pin-helper-tools.ts <fd|rg|uv> <version>");
process.exit(2);
}
const current = HELPER_TOOL_RELEASES[tool as HelperToolId];
const tag = current.tag.startsWith("v") ? `v${version}` : version;
const lines: string[] = [];
let failed = false;
for (const [platform, architecture] of SUPPORTED_TARGETS) {
const assetName = current.assetName(platform, architecture)?.replaceAll(current.version, version);
if (!assetName) continue;
const url = `https://github.com/${current.repo}/releases/download/${tag}/${assetName}`;
const digest = createHash("sha256").update(await fetchBytes(url)).digest("hex");
let note = "no upstream checksum published";
try {
const published = new TextDecoder().decode(await fetchBytes(`${url}.sha256`));
const match = published.match(/[0-9a-f]{64}/i)?.[0].toLowerCase();
note = match === digest ? "matches upstream .sha256" : `MISMATCH with upstream .sha256 (${match ?? "unparseable"})`;
if (match !== digest) failed = true;
} catch {
// Only ripgrep and uv publish per-asset checksum files.
}
lines.push(`\t\t\t"${assetName}": "${digest}", // ${note}`);
}
console.log(`// ${current.repo} ${tag}\n${lines.join("\n")}`);
if (failed) {
console.error("Upstream checksum mismatch detected; do not pin this release.");
process.exit(1);
}
}

await main();
58 changes: 44 additions & 14 deletions packages/coding-agent/src/core/kernel/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { stderr, stdin } from "node:process";
import { createInterface } from "node:readline/promises";
import { setTimeout as sleep } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { getPackageDir } from "../../config.js";
import { getBinDir, getPackageDir } from "../../config.js";
import { isProcessAlive, spawnHidden } from "../../utils/child-process.js";
import { tryAcquireDirLock } from "../../utils/dir-lock.js";
import { installPinnedHelperTool } from "../../utils/helper-tool-install.js";
import { HELPER_TOOL_RELEASES } from "../../utils/helper-tool-releases.js";
import type { PythonSkillRuntimeInfo } from "../skills.js";

const BOOTSTRAP_SCHEMA = 9;
Expand Down Expand Up @@ -71,7 +73,9 @@ export function buildBatchShimInvocation(
};
}

const UV_INSTALL_COMMAND = "curl -LsSf https://astral.sh/uv/install.sh | sh";
const UV_RELEASE = HELPER_TOOL_RELEASES.uv;
const UV_INSTALL_DOCS_URL = "https://docs.astral.sh/uv/getting-started/installation/";
const UV_DOWNLOAD_TIMEOUT_MS = 120_000;
const REQUIRED_HARNESS_METHODS = [
"create_memory",
"update_memory",
Expand Down Expand Up @@ -542,35 +546,57 @@ async function findExecutable(name: string): Promise<string | null> {
return null;
}

function uvBinaryFileName(): string {
return process.platform === "win32" ? "uv.exe" : "uv";
}

async function uvWorks(uv: string): Promise<boolean> {
try {
await run(uv, ["--version"], { stdio: "ignore" });
return true;
} catch {
return false;
}
}

async function ensureUv(options: EnsureKernelPythonOptions): Promise<string> {
const fromPath = await findExecutable("uv");
if (fromPath) return fromPath;

const localUv = path.join(os.homedir(), ".local", "bin", process.platform === "win32" ? "uv.exe" : "uv");
const managedUv = path.join(getBinDir(), uvBinaryFileName());
if (await isExecutable(managedUv)) return managedUv;

const localUv = path.join(os.homedir(), ".local", "bin", uvBinaryFileName());
if (await isExecutable(localUv)) return localUv;

const shouldInstallUv =
process.env.PRIME_AGENT_INSTALL_UV === "1" || (!options.onProgress && (await confirmUvInstall()));
if (!shouldInstallUv) {
throw new Error(
`uv is required to set up the Python kernel. Install uv yourself: ${UV_INSTALL_COMMAND}, ` +
"or set PRIME_AGENT_INSTALL_UV=1 to let prime-agent run that installer.",
`uv is required to set up the Python kernel. Install uv yourself (${UV_INSTALL_DOCS_URL}), ` +
`or set PRIME_AGENT_INSTALL_UV=1 to let prime-agent download uv ${UV_RELEASE.version} from GitHub releases with SHA-256 verification.`,
);
}

reportProgress(options, "› installing uv (one-time)…");
reportProgress(options, `› installing uv ${UV_RELEASE.version} (one-time)…`);
try {
await run("sh", ["-c", UV_INSTALL_COMMAND], { stdio: options.onProgress ? "ignore" : "inherit" });
// Downloads the pinned release archive from GitHub and checks its bundled SHA-256
// before extraction; nothing from the network is ever piped into a shell.
return await installPinnedHelperTool({
tool: "uv",
platform: process.platform,
architecture: process.arch,
destDir: getBinDir(),
binaryFileName: uvBinaryFileName(),
verifyBinary: uvWorks,
timeoutMs: UV_DOWNLOAD_TIMEOUT_MS,
});
} catch (error) {
throw new Error(
`couldn't install uv from astral.sh; install it yourself: ${UV_INSTALL_COMMAND}, then re-run prime-agent. ${errorMessage(error)}`,
`couldn't install uv ${UV_RELEASE.version} from ${UV_RELEASE.repo} releases: ${errorMessage(error)}. ` +
`Install uv yourself (${UV_INSTALL_DOCS_URL}), then re-run prime-agent.`,
);
}

if (await isExecutable(localUv)) return localUv;
const installedFromPath = await findExecutable("uv");
if (installedFromPath) return installedFromPath;
throw new Error("uv install completed but binary not found at ~/.local/bin/uv");
}

async function confirmUvInstall(): Promise<boolean> {
Expand All @@ -579,7 +605,11 @@ async function confirmUvInstall(): Promise<boolean> {

const rl = createInterface({ input: stdin, output: stderr });
try {
const answer = (await rl.question("Prime Agent needs uv to set up Python. Install uv from astral.sh now? [Y/n] "))
const answer = (
await rl.question(
`Prime Agent needs uv to set up Python. Download uv ${UV_RELEASE.version} from GitHub releases (SHA-256 verified) now? [Y/n] `,
)
)
.trim()
.toLowerCase();
return answer !== "n" && answer !== "no";
Expand Down
Loading
Loading