Repository navigation
Updates check the binary against checksums Plannotator downloads itself - #2218
Merged
Merged
Conversation
…ecksums itself On macOS and Linux the update wrapper no longer downloads with curl. The running binary's internal `update-prepare` fetches install.sh and both CPU types' .sha256 into a private temp dir; the wrapper runs that script with PLANNOTATOR_EXPECTED_SHA256, and install.sh exits 3 on a mismatch before it replaces anything. A failed download runs nothing (exit 75). Also: sem v0.8.0 checksums pinned in all three installers; a 1/3/7-day auto-update back-off after a mismatch; no curl | bash line offered after exit 3 or 75 (plannotator update, the Inbox failure dialog); tests and docs.
- update-prepare requires only this machine's CPU type's .sha256 (the one install.sh picks); the other is pinned when readable, so a release that drops one CPU type never stops updates for the other. - A checksum mismatch is install.sh exit 113 plus a marker file the wrapper names; the wrapper records 113 only with the marker and any other 113 as 1. The codes live in @plannotator/core/update-exit-codes, shared by the CLI, the Inbox failure dialog and the notice. - Tests: the letter-case pin asserts the pin path; update-prepare through the index.ts entrypoint; release.yml smoke-binaries runs scripts/smoke-update-prepare.sh on the compiled binary (update-prepare and a full plannotator update through the wrapper, loopback only).
Conflicts: - packages/server/auto-update.ts: POSIX_WRAPPER keeps main's detached lock line (pid/group/host/boot/pidns) first, then this branch's update-prepare path in place of curl (main's curl timeouts are covered by update-prepare's own 30 s fetch timeout). - docs/agent-guides/environment.md, inbox-foundation.md: both sides' lines. packages/server/auto-update-stuck.test.ts (from main) ran the wrapper through curl and PLANNOTATOR_UPDATE_SCRIPT_URL; it now serves a fake release to update-prepare-entry.ts, as auto-update.test.ts does.
Conflict: packages/server/auto-update.test.ts, the group-SIGINT test. Kept this branch's fakeRelease (install script + .sha256 server) with main's `exec sleep 30` deflake (#2237). The same `exec` goes into update-prepare.test.ts's Ctrl-C test, which has the same race.
- update-prepare.test.ts "a bad .sha256 means the script never runs" served the bad checksum for arm64 only. On an x64 runner that is the other CPU type's, which update-prepare rightly skips with a note, so the script ran. The bad one is now this machine's CPU type. - inbox-updates.spec.ts served only a bare script. Update now goes through update-prepare now, which needs Plannotator's install script and the .sha256 files: the fake release serves both, and the Inbox gets PLANNOTATOR_RELEASE_DOWNLOAD_URL. - The test preload and inbox-world point PLANNOTATOR_RELEASE_DOWNLOAD_URL at a closed loopback port, next to PLANNOTATOR_INSTALL_SCRIPT_URL, so an unintended update-prepare never asks GitHub.
Final review of #2218: - A certificate error from Bun's own roots is retried once with Bun's roots plus this computer's (tls.getCACertificates("system"): the macOS keychains' trusted certificates, Linux's /etc/ssl/certs), never emSign ECC Root CA - G3. A company root that curl trusted on main now works here too. Bun still refuses the compromised eMudhra keys whatever the roots (CERT_REVOKED, never retried), so there is no curl fallback: an attacker could force one, and macOS curl accepts their chain. - Exit 75 now shows the way out in `plannotator update` and the Inbox dialog: the install command and the release page, plus NODE_EXTRA_CA_CERTS when the certificate was not trusted. A revoked certificate never gets the curl line ("try another network"). 113 is unchanged. - A relative TMPDIR no longer fails every update (resolve()). - A test pins the .sha256 names release.yml publishes to candidateAssets, and the release skill says they never change. - Comment: the mismatch code is 113, not 3.
…n the retry Owner-approved follow-ups to #2218: - On macOS an "untrusted certificate" failure lists the release page before the `curl | bash` line, in `plannotator update` and in the Inbox dialog (`failure.release_first`, set by the server on darwin only). macOS curl still trusts emSign G3, so a network holding a leaked key that reads as untrusted rather than revoked never sends people to curl first. Revoked is unchanged (no curl line); other platforms keep the order. - The one retry after a certificate error also trusts what only curl read on main: $CURL_CA_BUNDLE and `cacert` / `capath` in the first .curlrc of $CURL_HOME, $XDG_CONFIG_HOME, $HOME. Absolute paths only, files capped at 4 MB (capath 16 MB in all), missing or unreadable paths skipped silently, emSign G3 filtered. Nothing is read unless a certificate error happened, so the normal path is unchanged.
…full temp folder or the old-script note From the end-to-end friction run on #2218: - The other CPU type's .sha256 is optional: once install.sh and this machine's .sha256 are in, it gets 2 s (OPTIONAL_CHECKSUM_GRACE_MS) and is then skipped with a note. A stall there no longer adds 30 s. - The NODE_EXTRA_CA_CERTS hint is now complete. When it names a readable certificate, update-prepare stages curl-ca-bundle.pem (curl's default bundle, SSL_CERT_FILE, Bun's and the system's roots, curl's own CA paths, that certificate; never emSign G3), and the wrapper hands it to install.sh as CURL_CA_BUNDLE, so the binary download trusts the same root. A bundle replaces curl's roots, so it only adds to them. Without the variable, curl is left exactly as before. - A temp folder that can't hold the update (ENOSPC, read-only, missing) still exits 75 but says so in plain words: "Couldn't prepare the update: the temp folder /tmp is full (ENOSPC)" in the CLI, the Inbox dialog (failure.prepare) and the review-window notice (reason "prepare"), never "couldn't download". - The "this install script does not check the pinned checksum yet" note goes to update.log only, not the person's terminal. The log-line texts and readers move to packages/server/update-log.ts (re-exported from update-prepare.ts), so auto-update.ts can read them without an import cycle.
update-prepare already says why nothing ran (a download, a certificate, a temp folder that can't hold the update). The wrapper's line after it said "could not download the update" even for a full temp folder; it now says only that nothing ran and nothing changed.
… records the result
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge after the Bun 1.4.4 PR and after 0.28.32 is cut (target 0.28.33)
0.28.33 must be built on Bun 1.4.4 (#2215 merges before then). The protection rests on the binary's own fetch rejecting the leaked eMudhra roots, which Bun 1.4.4 does and 1.3.14 does not.
What this does
Safe updates, step 1 (no signing keys). On macOS and Linux,
plannotator update, the Inbox's Update now and background auto-update no longer download anything with the systemcurl, which on macOS still trusts the leaked eMudhra root keys. The running Plannotator downloads the install script and the release's.sha256files with its own connection (Bun 1.4.4 blocks those keys). Then it runs the saved script, which checks the binary against those checksums before it replaces anything.plannotator update-prepare <version>(internal,packages/server/update-prepare.ts). It downloadsinstall.shand the.sha256of the binary install.sh will pick on this machine (process.arch; required) into a privateplannotator-update-*temp dir (0700, files 0600) and prints the dir. The other CPU type's.sha256is pinned when readable and skipped otherwise, so a release that drops one CPU type never stops updates for the other. It keeps the script only when it looks like ours (#!,REPO="backnotprop/plannotator", under 4 MB). Each checksum must be 64 hex characters. On any failure nothing is staged and it exits 75.packages/server/auto-update.ts). It runsupdate-prepare, thenbash <dir>/install.sh …withPLANNOTATOR_EXPECTED_SHA256. It removes the dir on exit, on Ctrl-C and on TERM, and keeps the lock, result and log contract. If the download fails, nothing runs and the result is 75. It never falls back tocurl. It removes the printed dir only when that dir is an absolute path on one line whose last part isplannotator-update-*.install.sh. WhenPLANNOTATOR_EXPECTED_SHA256has a valid entry for the binary it picked, the script uses that entry as the expected checksum and does not fetch the.sha256again. On a mismatch it prints a plain message, replaces nothing, touches the marker file the wrapper names (PLANNOTATOR_UPDATE_MISMATCH_FILE) and exits 113. The wrapper records 113 only when that marker exists; any other exit 113 is recorded as 1, so "nothing was changed" is never claimed for another error. The codes live inpackages/core/update-exit-codes.ts, shared by the CLI, the Inbox dialog and the notice. If the entry is missing or malformed, it prints a note and falls back to today's check. Without the variable, the script behaves exactly as before.semsidecar. The v0.8.0 archive checksums are pinned ininstall.sh,install.ps1andinstall.cmdinstead of read fromchecksums.txt. I cross-checked the pinned values againstchecksums.txtand against the GitHub API's assetdigest.mismatchBackoffinupdate-state.json, outcomebacked-off). A newer release is tried at once. Update now andplannotator updateignore the back-off.plannotator updatenor the Inbox's failure dialog prints thecurl | bashline. The review-window toast says "Update to vX stopped" once per version. A background download failure shows nothing, because nothing changed.Windows is unchanged: Windows blocks the leaked keys in its own certificate store, which
Invoke-WebRequestuses. The Pi and OpenCode runtimes have no installer path. Pi vendorsupdate-check.ts, where the only change is one additive optional field.Deploy note: plannotator.ai must serve the new install.sh
The binary check only takes effect once
https://plannotator.ai/install.shserves this branch'sscripts/install.sh. The owner syncs it to S3 by hand. Until then, updates behave exactly as today, because an older script ignoresPLANNOTATOR_EXPECTED_SHA256. In that caseupdate-preparelogs "this install script does not check the pinned checksum yet". The release checklist (.agents/skills/release/SKILL.md) now has this step.Tests
packages/server/update-prepare.test.ts:v;.sha256, a captive portal, a script that is not ours, an oversized script, a stalled server;.sha256;install.sh(mismatch recorded, old binary kept);.sha256(still updates);install.sh;install.sh(runs as before);plannotator-update-*).scripts/install.test.ts: a matching pin, the pin read for this CPU type only and in any letter case (asserting the pin path), a pin for this CPU type alone, tamper (exit 113 and the marker, old binary kept), the marker only at a…/checksum-mismatchpath, malformed pins falling back, no variable meaning no change, and thesempins.packages/server/auto-update.test.ts: the wrapper tests moved to a loopback release server; the killed-outright test checks the sweep; there are tests for the 1/3/7-day back-off and the mismatch and download notices.apps/hook/server/update-command.test.ts: exit 113 and 75 messages (no install command); the process tests check the pins passed in and a download failure;update-preparethrough the realindex.tsentrypoint (stdout is exactly the dir; 75 with empty stdout on failure).release.ymlsmoke-binaries(Linux and macOS):scripts/smoke-update-prepare.shruns the compiled binary'supdate-prepareagainst a loopback mirror, then a fullplannotator updatethrough the wrapper (compiled-binary detection) with a stand-in install script.packages/inbox/updates.test.tsx,packages/ui/utils/autoUpdateNotice.test.ts: the dialog and the toast for 113 and 75.Sandboxed: temp HOME, TMPDIR and data dir, loopback servers and stub
curl. Nothing reaches the network.