verify self-update tarball origin and digest before installing - #2170
verify self-update tarball origin and digest before installing#2170kevinjosethomas wants to merge 1 commit into
Conversation
…e installing The self-update installed whatever tarball the release manifest named, on any origin or scheme, with no digest check and no package or version match. It also fell back to installing the bare package name from the npm registry, where the public package does not exist. The manifest tarball must now be same-origin https with the release base, named <package>-<version>.tgz for the installed package, and carry a SHA-256 digest. The tarball is downloaded to a temp dir and hashed before the package manager sees the local path; every rejection happens first and is reported. The pack script emits the top-level digest. Linear: ENG-5341
Prime Agent performance — completedPR
Python runtime
Sandbox cost: ~$0.1052 — no inference calls. Methodology and samplesMain resolved at 2026-09-09T23:53:44.687894+00:00. Harness
|
Linear: ENG-5341 — https://linear.app/primeintellect/issue/ENG-5341
Context
prime-agent updatefetchedlatest.json/beta.jsonand passed whatevertarballit named straight tonpm install -g <spec>(pnpm/yarn/bun variants too). Nothing checked the tarball origin or scheme (https://attacker.invalid/…,http://…,file:///…were all accepted), the manifestsha256digests were ignored, apackagediffering from the installed one was accepted (and the installed package uninstalled), and a version/filename mismatch went through. When the manifest was unreachable or had no tarball it fell back tonpm install -g <package name>from the npm registry, whereprime-agentis not published.install.shalready verifies againstSHA256SUMSand was left untouched.Root cause:
getLatestPiRelease(src/utils/version-check.ts) returned an unvalidatedinstallSpec, andgetSelfUpdatePlan(src/package-manager-cli.ts) handed it togetSelfUpdateCommand, whoseisDirectPackageArtifactSpectreats any http/https/file spec as an installable artifact.Changes
src/utils/version-check.ts: the manifest is now validated. Base URL must be https (http://only with the documentedPRIME_AGENT_ALLOW_INSECURE_DOWNLOAD_BASE_URL=1;file:never). The tarball must be same-origin with the release base, be named<package>-<version>.tgzfor the installed package (or an allowlisted rename), and carry a SHA-256 digest (top-levelsha256or a matchingtarballs[]entry).packagemust equal the installed name. Failures throwReleaseManifestError; the startup notice swallows it and stays quiet.src/utils/self-update-artifact.ts(new): downloads the tarball to a fresh temp dir withredirect: "error", hashes it while streaming, discards it on mismatch, and returns the local path plus a cleanup handle.src/package-manager-cli.ts: the plan requires a verified tarball; the package manager is invoked with the verified local path, never a URL; the registry-name fallback is removed; every rejection is reported asError: …with exit code 1 before any package manager call.updateis an explicit request, so it now fetches the manifest even underPI_SKIP_VERSION_CHECK/PI_OFFLINE(it already hit the network via npm before).scripts/pack-prime-agent-release.mjs: emits a top-levelsha256for the CLI tarball (thetarballs[].sha256entries it already wrote are also accepted, so current published manifests verify as-is).docs/settings.md,docs/usage.md,README.md) describe the new manifest requirements and the insecure-mirror override; changelog fragment added.Model-facing surface (tool names, system prompt, kernel API) is untouched.
Validation
Local:
npm run checkpasses (biome, tsgo, check-installer, browser smoke). Focused tests frompackages/coding-agent:New coverage:
version-check.test.ts(accept + each rejection: foreign origin, http/file tarball, http base without/with override, file base, package mismatch, rename allowlist, version/filename mismatch, missing/malformed digest, bad version, startup notice quiet, explicit bypass),self-update-artifact.test.ts(verified download, digest mismatch discard, HTTP error, network error, cleanup),package-self-update-daemon.test.ts(installs the local verified path, and six rejection cases plus unreachable manifest assert nospawn:before exit 1).Prime Sandbox (
node:24-bookworm, usertester, fresh HOME, localhttp://127.0.0.1:18555mirror, fake globalnpmthat records invocations; both trees run throughhandlePackageCommand(["update","--self"])via tsx):npm install -g http://127.0.0.1:18555/…/earendil-works-pi-coding-agent-99.0.0.tgznpm install -g /tmp/prime-agent-update-XXXX/earendil-works-pi-coding-agent-99.0.0.tgz, fake npm hashed the file to the manifest digest, temp dir removedError: SHA-256 mismatch … The download was discarded.exit 1, npm not invokedhttps://attacker.invalid/pkg-99.0.0.tgznpm install -g https://attacker.invalid/pkg-99.0.0.tgzError: … not on the release originexit 1, no download, npm not invokedfile:///tmp/evil-99.0.0.tgznpm install -g file:///tmp/evil-99.0.0.tgzpackage: "@attacker/anything"npm install -g <url> && npm uninstall -g @earendil-works/pi-coding-agentError: … names package "@attacker/anything", but this installation is …exit 1…-99.0.0.tgz)Error: Release downloads require an https base URL …exit 1, no fetchExisting tests in the sandbox: 100/100 on this branch; main baseline 71/71 for the same four pre-existing files. Sandbox deleted afterwards.
Not validated: a real
npm install -g <local tarball>against the R2 bucket (fake npm only), and pnpm/yarn/bun accepting an absolute local.tgzpath (npm does; yarn v1 may prefer afile:prefix — worth a check before release). Internal dependency tarballs referenced by URL from the releasepackage.jsonare still fetched by npm itself, as withinstall.sh; verifying those would need the manifest's othertarballs[]digests and a rewritten package.json — follow-up.Note
Verify self-update tarball origin and digest before installing
parseReleaseManifestandgetLatestPiRelease: package names must match the installed package, tarball URLs must be same-origin HTTPS, filenames must match package and version, and digests must be valid 64-char hex SHA-256getPrimeAgentDownloadBaseUrlotherwise requires HTTPS and rejectsfile:URLsgetSelfUpdatePlanno longer returns a registry install spec or falls back on manifest failure; out-of-tree consumers of the old plan shape will break.resolveTarballUrlandfindManifestDigestreject foreign-origin or digest-less tarballs that were previously acceptedMacroscope summarized 0c1ac22.