From 564b6915a5afbea46f82fe6a323ff3f26d5e5950 Mon Sep 17 00:00:00 2001 From: Jono Minh <2witstudios@gmail.com> Date: Thu, 22 Oct 2026 08:12:21 -0500 Subject: [PATCH 1/2] build(runtime): move the bundled and CI Node to 26 Repins every setup-node step, the bundled Point Guard runtime (darwin + linux archives with SHA-256), engines and @types/node 26. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QE3fBBjeceQdEHtXSDtcNF --- .github/scripts/test_release_workflow.py | 2 +- .github/workflows/macos.yml | 12 ++++++------ .github/workflows/release-proof.yml | 2 +- .github/workflows/release.yml | 6 +++--- apps/purepoint-mobile/README.md | 4 ++-- apps/purepoint-mobile/package-lock.json | 18 +++++++++--------- apps/purepoint-mobile/package.json | 4 ++-- .../runtime/bundled-pi-verification.md | 2 +- .../purepoint-mobile/runtime/node-release.json | 18 +++++++++--------- docs/architecture/distribution.md | 2 +- docs/product/linux-phone-host.md | 4 ++-- 11 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 1ddfdc37..826d0ddc 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -257,7 +257,7 @@ def test_dmg_creation_failures_are_not_ignored(self): def test_packaging_uses_the_same_node_as_ci(self): node = [step["with"] for step in steps(self.jobs["build-macos"]) if step.get("uses", "").startswith("actions/setup-node@")] self.assertEqual(len(node), 1) - self.assertEqual(str(node[0]["node-version"]), "22.23.3") + self.assertEqual(str(node[0]["node-version"]), "26.12.1") self.assertIs(node[0]["package-manager-cache"], False) def test_build_job_produces_an_unsigned_app(self): diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 41639842..e60edfd7 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -66,7 +66,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' cache: npm cache-dependency-path: apps/purepoint-mobile/package-lock.json - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master @@ -104,7 +104,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' cache: npm cache-dependency-path: apps/purepoint-mobile/package-lock.json - run: npm ci --ignore-scripts @@ -241,7 +241,7 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' - name: Prepare actual shared bridge dependencies and isolated helper run: | npm ci --ignore-scripts --prefix apps/purepoint-mobile @@ -312,7 +312,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: toolchain: "1.89" @@ -364,7 +364,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: point-guard-runtime-arm64 @@ -430,7 +430,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: toolchain: "1.89" diff --git a/.github/workflows/release-proof.yml b/.github/workflows/release-proof.yml index 9079d9c8..f4dd0134 100644 --- a/.github/workflows/release-proof.yml +++ b/.github/workflows/release-proof.yml @@ -28,7 +28,7 @@ jobs: targets: aarch64-apple-darwin,x86_64-apple-darwin - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' - name: Isolate archive Rust output run: printf 'CARGO_TARGET_DIR=%s/release-cargo\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" # Pinned release, checked against its SHA-256 instead of Homebrew's moving formula. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 042ffd04..73dead69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: - name: Install Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' package-manager-cache: false # Ad-hoc signed only; the sign-macos job re-signs every nested binary @@ -478,7 +478,7 @@ jobs: - name: Install Node for npm uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' package-manager-cache: false - name: Download Linux build @@ -559,7 +559,7 @@ jobs: - name: Install Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '22.23.3' + node-version: '26.12.1' package-manager-cache: false # Installed outside the repository so its own lockfile and the 1.94.0 diff --git a/apps/purepoint-mobile/README.md b/apps/purepoint-mobile/README.md index 183f25d8..ee70fa4e 100644 --- a/apps/purepoint-mobile/README.md +++ b/apps/purepoint-mobile/README.md @@ -20,9 +20,9 @@ The service runs for the Mac app lifetime. Restart/update restores selected nati ## Development and package proof -The lockfile pins vanilla `@earendil-works/pi-coding-agent` **1.1.0** and `ws` **8.22.0**. Development requires Node22.19+ and `npm ci --ignore-scripts`; this is separate from end-user installation. Do not globally install the harness or overwrite running pu binaries. The managed launcher uses bundled versioned support/instructions and explicit CLI routing; existing native Pi resources remain compatible. Cross-project operations use `PU_PROJECT_ROOT=/absolute/project pu ...` or an explicit cwd, never an inherited builder identity. +The lockfile pins vanilla `@earendil-works/pi-coding-agent` **1.1.0** and `ws` **8.22.0**. Development requires Node 26+ and `npm ci --ignore-scripts`; this is separate from end-user installation. Do not globally install the harness or overwrite running pu binaries. The managed launcher uses bundled versioned support/instructions and explicit CLI routing; existing native Pi resources remain compatible. Cross-project operations use `PU_PROJECT_ROOT=/absolute/project pu ...` or an explicit cwd, never an inherited builder identity. -`runtime/package-runtime.py` stages the supported Node22.23.3, complete production graph and pu helper into an app artifact. `runtime/sign-runtime.py` signs nested code before the outer app, with Node JIT entitlement. Nonpublishing CI (arm64 on pull requests that touch packaging inputs; x64 and universal on `main`, nightly and manual runs) verifies arm64, x64 and universal resources, signatures and relocated empty-HOME/stripped-PATH launch, then state-preserving bundle replacement. Ad-hoc proof does not establish Developer ID/notarization or physical-device acceptance. The normative [setup/trust contract](../../docs/product/point-guard-setup.md) defines private state and listener/admin boundaries. +`runtime/package-runtime.py` stages the supported Node26.12.1, complete production graph and pu helper into an app artifact. `runtime/sign-runtime.py` signs nested code before the outer app, with Node JIT entitlement. Nonpublishing CI (arm64 on pull requests that touch packaging inputs; x64 and universal on `main`, nightly and manual runs) verifies arm64, x64 and universal resources, signatures and relocated empty-HOME/stripped-PATH launch, then state-preserving bundle replacement. Ad-hoc proof does not establish Developer ID/notarization or physical-device acceptance. The normative [setup/trust contract](../../docs/product/point-guard-setup.md) defines private state and listener/admin boundaries. ## iPhone / Xcode — owner-run diff --git a/apps/purepoint-mobile/package-lock.json b/apps/purepoint-mobile/package-lock.json index 293f7cb7..24cba4f0 100644 --- a/apps/purepoint-mobile/package-lock.json +++ b/apps/purepoint-mobile/package-lock.json @@ -11,13 +11,13 @@ "ws": "8.22.0" }, "devDependencies": { - "@types/node": "22.19.19", + "@types/node": "26.6.5", "@types/ws": "8.18.2", "prettier": "3.9.10", "typescript": "5.9.3" }, "engines": { - "node": ">=22.19.0" + "node": ">=26.0.0" } }, "node_modules/@anthropic-ai/sdk": { @@ -685,12 +685,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/retry": { @@ -1713,9 +1713,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "license": "MIT" }, "node_modules/web-streams-polyfill": { diff --git a/apps/purepoint-mobile/package.json b/apps/purepoint-mobile/package.json index 6d474b38..8370d7fd 100644 --- a/apps/purepoint-mobile/package.json +++ b/apps/purepoint-mobile/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "engines": { - "node": ">=22.19.0" + "node": ">=26.0.0" }, "scripts": { "start": "node bridge/main.js", @@ -20,7 +20,7 @@ "ws": "8.22.0" }, "devDependencies": { - "@types/node": "22.19.19", + "@types/node": "26.6.5", "@types/ws": "8.18.2", "prettier": "3.9.10", "typescript": "5.9.3" diff --git a/apps/purepoint-mobile/runtime/bundled-pi-verification.md b/apps/purepoint-mobile/runtime/bundled-pi-verification.md index 3d63e9f6..81f441f3 100644 --- a/apps/purepoint-mobile/runtime/bundled-pi-verification.md +++ b/apps/purepoint-mobile/runtime/bundled-pi-verification.md @@ -1,6 +1,6 @@ # Bundled Pi verification — 2026-10-10 -Point Guard owns its Pi runtime. Users install and update PurePoint; they do not need to install or update Node, npm, or Pi separately. The app manifest pins Pi 1.1.0 and Node 22.23.3. The native owner launches the explicit bundled Node helper; the bridge launches that same executable with its bundled Pi adapter and package. Canonical Pi credentials and sessions remain outside the app so app updates preserve them. +Point Guard owns its Pi runtime. Users install and update PurePoint; they do not need to install or update Node, npm, or Pi separately. The app manifest pins Pi 1.1.0 and Node 26.12.1. The native owner launches the explicit bundled Node helper; the bridge launches that same executable with its bundled Pi adapter and package. Canonical Pi credentials and sessions remain outside the app so app updates preserve them. Managed RPC startup/exit recovery now directs users to Point Guard setup and PurePoint retry/update/reinstallation. Development bridge startup retains npm guidance. Missing-model feedback asks for provider/model configuration without requiring a separate terminal Pi installation. diff --git a/apps/purepoint-mobile/runtime/node-release.json b/apps/purepoint-mobile/runtime/node-release.json index ba372bdd..58b19f4a 100644 --- a/apps/purepoint-mobile/runtime/node-release.json +++ b/apps/purepoint-mobile/runtime/node-release.json @@ -1,21 +1,21 @@ { - "version": "22.23.3", + "version": "26.12.1", "archives": { "arm64": { - "name": "node-v22.23.3-darwin-arm64.tar.gz", - "sha256": "23b25245dcfb9af7262f8ff142e9e2e0af025368117329e7a7458a51e5922f53" + "name": "node-v26.12.1-darwin-arm64.tar.gz", + "sha256": "1901d539912c02afa10d1cc126943a8855a7eedebe830119463470db5e6c59e8" }, "x64": { - "name": "node-v22.23.3-darwin-x64.tar.gz", - "sha256": "8a677b0219178efd6eb0e475457c4afb452b521a92f6e67845a73bd85727f2a8" + "name": "node-v26.12.1-darwin-x64.tar.gz", + "sha256": "b85c2ea4420ad0bc4abf2dc6cf1f55130ed681f1cabf60cf3f526e4c9e2712ff" }, "linux-arm64": { - "name": "node-v22.23.3-linux-arm64.tar.gz", - "sha256": "5ced2d48d1d7198739b7f86804de0171aefb6823b684b12341d3321afc3cb0b2" + "name": "node-v26.12.1-linux-arm64.tar.gz", + "sha256": "a6c01af459dd8972ab3fb08ee5da81244d26ba8c4944a62d5bbba8c597ae154f" }, "linux-x64": { - "name": "node-v22.23.3-linux-x64.tar.gz", - "sha256": "1084aa36196bba4c3a5e69a1ee388a6e4ff729dad09445fbcd434b28fe3c24af" + "name": "node-v26.12.1-linux-x64.tar.gz", + "sha256": "0fd1eb7770598f52a21995edf6ed35fc690400fa437c2386af329519e9143132" } } } diff --git a/docs/architecture/distribution.md b/docs/architecture/distribution.md index d4d0aeb3..65cb462b 100644 --- a/docs/architecture/distribution.md +++ b/docs/architecture/distribution.md @@ -12,7 +12,7 @@ PurePoint needs to ship to users as a self-contained product with no external ru ! [DIST-004] Linux servers get static musl builds of `pu` and `pu-engine` (`x86_64-` and `aarch64-unknown-linux-musl`), one `purepoint--.tar.gz` (plus `.sha256`) per target, attached to the same GitHub release as the DMG. They are built natively per architecture in `release.yml`. Each tarball also gets a minisign signature (`.tar.gz.minisig`) from the release key and a GitHub build provenance attestation. Each tarball also carries the static `pu-point-guard-lock` helper that the phone access payload ships (DIST-008). `scripts/install.sh` checks the checksum, then verifies who built the download before installing anything: the signature must verify against the minisign public key embedded in `install.sh` (with `minisign`, or with OpenSSL 3 when minisign isn't installed), and when the GitHub CLI is installed and signed in, `gh attestation verify --repo 2witstudios/PurePoint` must pass. At least one of the two must succeed and any check that runs must pass, so neither check depends only on files from the same release. If neither can run (no signature on the release, or no minisign, OpenSSL 3 or `gh`), the install stops unless `PU_INSTALL_SKIP_VERIFY=1` is set, which installs with a warning. The opt-out never overrides a check that failed. A local `PU_ARCHIVE` is installed as given. The script installs both binaries side by side in `~/.pu/bin`, replacing each by rename so a running daemon's binary is never overwritten in place. There's no package-manager integration yet. -! [DIST-005] Releases are built and signed in separate jobs (`.github/workflows/release.yml`, run only by a `v*` tag push). `build-macos` and `linux` compile every third-party dependency with a read-only token, no secrets and no restored cache; the macOS app is archived ad-hoc signed. `sign-macos` runs in the protected `release` environment and is the only job that receives secrets. It runs no build code: only Apple's tools, `.github/scripts/sign-macos-app.sh` (inside-out Developer ID signing, then a check that every Mach-O in the bundle has the identity, the hardened runtime and a secure timestamp), and the Sparkle and create-dmg releases, each fetched with `curl -fsSL` and checked against a SHA-256 committed in the workflow. The Sparkle tools match the version the app links in `Package.resolved`. The certificate is imported into a temporary keychain as non-extractable and usable only by `/usr/bin/codesign`, the keychain locks after 15 idle minutes, and the keychain and `.p12` are deleted after the last signature and again in an `always()` cleanup step. `build-macos` uses the same Node (22.23.3, via `actions/setup-node`) as CI, and every `xcodebuild` that builds the app in CI or a release passes `-onlyUsePackageVersionsFromResolvedFile`, so Swift packages resolve only to the committed `Package.resolved`. Every remote Swift package is pinned to a version or a revision, never a branch. `create-dmg` failures fail the release: a non-zero exit is tolerated only when the DMG was still written and passes `hdiutil verify`, with a warning. `sign-linux` also runs in the `release` environment. It signs the Linux tarballs with `.github/scripts/minisign.sh` (minisign-format Ed25519 signatures made with OpenSSL, so no signing tool is downloaded) after checking that the `RELEASE_MINISIGN_KEY` secret matches the public key in `scripts/install.sh`. Without that secret the job skips signing with a warning and the release ships without `.minisig` files. `sbom` writes CycloneDX SBOMs for `pu` and `pu-engine` (`cargo cyclonedx --target all`, pinned version) and for the Point Guard npm runtime (`npm sbom --omit=dev`). `attest` (`id-token`, `attestations` and `artifact-metadata` write, no checkout, no `run` steps) records build provenance for the Linux tarballs and the DMG with `actions/attest`, and attests the SBOMs against them; verify with `gh attestation verify --repo 2witstudios/PurePoint`. `publish` is the only job with `contents: write` and runs only after both signing jobs and `attest` succeed. It checks out no code, creates the GitHub release with the DMG, the Linux tarballs and phone access payloads (DIST-008) and their signatures, and the SBOMs in one step, and never replaces a versioned asset. The `appcast.xml` on the `appcast` release is the moving update feed and is the one asset replaced on each release. The unsigned app and the DMG pass between jobs as workflow artifacts kept for 14 days, so a release can wait that long for environment approval and still be re-run. If the release already exists when `publish` runs again, every asset must match the published one byte for byte (by SHA-256), or the job fails before touching the appcast. Re-running every job re-signs the DMG and changes its bytes, so use "Re-run failed jobs", or delete the release first. Every action in every workflow is pinned to a full commit SHA with a version comment, and no checkout keeps the token in `.git/config`. `.github/scripts/test_release_workflow.py` checks these properties in CI, and `.github/scripts/test_install_script.py` checks the install script's verification against signatures made by `minisign.sh` (and, when `minisign` is installed, that minisign itself accepts them). +! [DIST-005] Releases are built and signed in separate jobs (`.github/workflows/release.yml`, run only by a `v*` tag push). `build-macos` and `linux` compile every third-party dependency with a read-only token, no secrets and no restored cache; the macOS app is archived ad-hoc signed. `sign-macos` runs in the protected `release` environment and is the only job that receives secrets. It runs no build code: only Apple's tools, `.github/scripts/sign-macos-app.sh` (inside-out Developer ID signing, then a check that every Mach-O in the bundle has the identity, the hardened runtime and a secure timestamp), and the Sparkle and create-dmg releases, each fetched with `curl -fsSL` and checked against a SHA-256 committed in the workflow. The Sparkle tools match the version the app links in `Package.resolved`. The certificate is imported into a temporary keychain as non-extractable and usable only by `/usr/bin/codesign`, the keychain locks after 15 idle minutes, and the keychain and `.p12` are deleted after the last signature and again in an `always()` cleanup step. `build-macos` uses the same Node (26.12.1, via `actions/setup-node`) as CI, and every `xcodebuild` that builds the app in CI or a release passes `-onlyUsePackageVersionsFromResolvedFile`, so Swift packages resolve only to the committed `Package.resolved`. Every remote Swift package is pinned to a version or a revision, never a branch. `create-dmg` failures fail the release: a non-zero exit is tolerated only when the DMG was still written and passes `hdiutil verify`, with a warning. `sign-linux` also runs in the `release` environment. It signs the Linux tarballs with `.github/scripts/minisign.sh` (minisign-format Ed25519 signatures made with OpenSSL, so no signing tool is downloaded) after checking that the `RELEASE_MINISIGN_KEY` secret matches the public key in `scripts/install.sh`. Without that secret the job skips signing with a warning and the release ships without `.minisig` files. `sbom` writes CycloneDX SBOMs for `pu` and `pu-engine` (`cargo cyclonedx --target all`, pinned version) and for the Point Guard npm runtime (`npm sbom --omit=dev`). `attest` (`id-token`, `attestations` and `artifact-metadata` write, no checkout, no `run` steps) records build provenance for the Linux tarballs and the DMG with `actions/attest`, and attests the SBOMs against them; verify with `gh attestation verify --repo 2witstudios/PurePoint`. `publish` is the only job with `contents: write` and runs only after both signing jobs and `attest` succeed. It checks out no code, creates the GitHub release with the DMG, the Linux tarballs and phone access payloads (DIST-008) and their signatures, and the SBOMs in one step, and never replaces a versioned asset. The `appcast.xml` on the `appcast` release is the moving update feed and is the one asset replaced on each release. The unsigned app and the DMG pass between jobs as workflow artifacts kept for 14 days, so a release can wait that long for environment approval and still be re-run. If the release already exists when `publish` runs again, every asset must match the published one byte for byte (by SHA-256), or the job fails before touching the appcast. Re-running every job re-signs the DMG and changes its bytes, so use "Re-run failed jobs", or delete the release first. Every action in every workflow is pinned to a full commit SHA with a version comment, and no checkout keeps the token in `.git/config`. `.github/scripts/test_release_workflow.py` checks these properties in CI, and `.github/scripts/test_install_script.py` checks the install script's verification against signatures made by `minisign.sh` (and, when `minisign` is installed, that minisign itself accepts them). ! [DIST-006] The update feed is signed. `sign-macos` generates `appcast.xml` and signs it with `sign_update` and the Sparkle EdDSA key that signs the DMG, so the feed and the update share one key. The app sets `SURequireSignedFeed` and `SUVerifyUpdateBeforeExtraction` (both need Sparkle 2.9 or later), so Sparkle rejects an unsigned or modified feed and checks the update's EdDSA signature before extracting it. With `SUVerifyUpdateBeforeExtraction`, the EdDSA key can't be rotated in an ordinary update; rotating it needs a release that turns the setting off first. Apps released before this change don't check the feed signature but still check each update's EdDSA signature and Apple team. diff --git a/docs/product/linux-phone-host.md b/docs/product/linux-phone-host.md index 3613bb01..a57b1828 100644 --- a/docs/product/linux-phone-host.md +++ b/docs/product/linux-phone-host.md @@ -100,9 +100,9 @@ Everything in the bridge and runtime packaging that assumes macOS or the Mac app #### [LPH-001] Node build and libc floor -Option A: official nodejs.org `linux-x64`/`linux-arm64` tarballs (glibc), the same 22.23.3 that the Mac pins. +Option A: official nodejs.org `linux-x64`/`linux-arm64` tarballs (glibc), the same 26.12.1 that the Mac pins. - Pro: the same upstream, version and SHA256SUMS flow as `node-release.json`; covers Debian/Ubuntu/Fedora/RHEL 8+, Amazon Linux 2023 and Raspberry Pi OS 64-bit. -- Con: needs glibc ≥ 2.28 (Node 22's Linux build floor). Alpine/musl hosts are unsupported, unlike the static musl `pu` binaries (DIST-004). +- Con: needs glibc ≥ 2.28 (Node 26's Linux build floor, unchanged from 22). Alpine/musl hosts are unsupported, unlike the static musl `pu` binaries (DIST-004). Option B: unofficial-builds musl Node. - Pro: runs on Alpine. From 9732c84fb248816ce3e33c40320c02714cfa0b77 Mon Sep 17 00:00:00 2001 From: Jono Minh <2witstudios@gmail.com> Date: Mon, 2 Nov 2026 09:44:07 -0600 Subject: [PATCH 2/2] build(runtime): move to Node 26 LTS (26.13.0) Repins every setup-node step, the bundled Point Guard runtime archives (darwin and linux, with nodejs.org SHA-256 checksums), engines and @types/node 26.6.6. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QE3fBBjeceQdEHtXSDtcNF --- .github/scripts/test_release_workflow.py | 2 +- .github/workflows/macos.yml | 12 ++++++------ .github/workflows/release-proof.yml | 2 +- .github/workflows/release.yml | 6 +++--- CHANGELOG.md | 1 + apps/purepoint-mobile/README.md | 2 +- apps/purepoint-mobile/package-lock.json | 8 ++++---- apps/purepoint-mobile/package.json | 2 +- .../runtime/bundled-pi-verification.md | 2 +- .../purepoint-mobile/runtime/node-release.json | 18 +++++++++--------- docs/architecture/distribution.md | 2 +- docs/product/linux-phone-host.md | 2 +- 12 files changed, 30 insertions(+), 29 deletions(-) diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 826d0ddc..2c845496 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -257,7 +257,7 @@ def test_dmg_creation_failures_are_not_ignored(self): def test_packaging_uses_the_same_node_as_ci(self): node = [step["with"] for step in steps(self.jobs["build-macos"]) if step.get("uses", "").startswith("actions/setup-node@")] self.assertEqual(len(node), 1) - self.assertEqual(str(node[0]["node-version"]), "26.12.1") + self.assertEqual(str(node[0]["node-version"]), "26.13.0") self.assertIs(node[0]["package-manager-cache"], False) def test_build_job_produces_an_unsigned_app(self): diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e60edfd7..9e5a3283 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -66,7 +66,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' cache: npm cache-dependency-path: apps/purepoint-mobile/package-lock.json - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master @@ -104,7 +104,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' cache: npm cache-dependency-path: apps/purepoint-mobile/package-lock.json - run: npm ci --ignore-scripts @@ -241,7 +241,7 @@ jobs: - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' - name: Prepare actual shared bridge dependencies and isolated helper run: | npm ci --ignore-scripts --prefix apps/purepoint-mobile @@ -312,7 +312,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: toolchain: "1.89" @@ -364,7 +364,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: point-guard-runtime-arm64 @@ -430,7 +430,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' - uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master with: toolchain: "1.89" diff --git a/.github/workflows/release-proof.yml b/.github/workflows/release-proof.yml index f4dd0134..6788e716 100644 --- a/.github/workflows/release-proof.yml +++ b/.github/workflows/release-proof.yml @@ -28,7 +28,7 @@ jobs: targets: aarch64-apple-darwin,x86_64-apple-darwin - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' - name: Isolate archive Rust output run: printf 'CARGO_TARGET_DIR=%s/release-cargo\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" # Pinned release, checked against its SHA-256 instead of Homebrew's moving formula. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73dead69..fba89fd2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,7 +106,7 @@ jobs: - name: Install Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' package-manager-cache: false # Ad-hoc signed only; the sign-macos job re-signs every nested binary @@ -478,7 +478,7 @@ jobs: - name: Install Node for npm uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' package-manager-cache: false - name: Download Linux build @@ -559,7 +559,7 @@ jobs: - name: Install Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: - node-version: '26.12.1' + node-version: '26.13.0' package-manager-cache: false # Installed outside the repository so its own lockfile and the 1.94.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a997dc..55e7bd7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to PurePoint are documented here. Format follows [Keep a Cha - Paste an image (Cmd+V) into any agent's terminal, or drop files on a remote agent's terminal, and the macOS app uploads it to the agent's machine and types its path there, so remote agents can see screenshots. Uploads are kept in `~/.pu/attachments` on that machine for 7 days. ### Changed +- Point Guard now runs on its bundled Node.js 26 LTS (26.13.0), up from Node 22, on macOS and on Linux phone hosts. Nothing to install: the runtime ships inside PurePoint. Linux phone hosts still need glibc 2.28 or newer. - The daemon protocol is now version 14, for the new `reorder_projects` request. Update PurePoint on every machine and restart its daemon. - Phones and iPads now pair with full access (agents, terminals, workspace management and Point Guard chat). Point Guard setup shows each device on one line with a single Access menu (Full access, Terminals only, View only, No access) and a Revoke button. Revoked devices are removed instead of lingering as "Revoked", and the redundant "Require re-pair" button is gone. Devices that are already paired keep their current access. - The daemon protocol is now version 12, for the new `attach_file` request. Update PurePoint on every machine and restart its daemon. diff --git a/apps/purepoint-mobile/README.md b/apps/purepoint-mobile/README.md index ee70fa4e..2228af6d 100644 --- a/apps/purepoint-mobile/README.md +++ b/apps/purepoint-mobile/README.md @@ -22,7 +22,7 @@ The service runs for the Mac app lifetime. Restart/update restores selected nati The lockfile pins vanilla `@earendil-works/pi-coding-agent` **1.1.0** and `ws` **8.22.0**. Development requires Node 26+ and `npm ci --ignore-scripts`; this is separate from end-user installation. Do not globally install the harness or overwrite running pu binaries. The managed launcher uses bundled versioned support/instructions and explicit CLI routing; existing native Pi resources remain compatible. Cross-project operations use `PU_PROJECT_ROOT=/absolute/project pu ...` or an explicit cwd, never an inherited builder identity. -`runtime/package-runtime.py` stages the supported Node26.12.1, complete production graph and pu helper into an app artifact. `runtime/sign-runtime.py` signs nested code before the outer app, with Node JIT entitlement. Nonpublishing CI (arm64 on pull requests that touch packaging inputs; x64 and universal on `main`, nightly and manual runs) verifies arm64, x64 and universal resources, signatures and relocated empty-HOME/stripped-PATH launch, then state-preserving bundle replacement. Ad-hoc proof does not establish Developer ID/notarization or physical-device acceptance. The normative [setup/trust contract](../../docs/product/point-guard-setup.md) defines private state and listener/admin boundaries. +`runtime/package-runtime.py` stages the supported Node26.13.0, complete production graph and pu helper into an app artifact. `runtime/sign-runtime.py` signs nested code before the outer app, with Node JIT entitlement. Nonpublishing CI (arm64 on pull requests that touch packaging inputs; x64 and universal on `main`, nightly and manual runs) verifies arm64, x64 and universal resources, signatures and relocated empty-HOME/stripped-PATH launch, then state-preserving bundle replacement. Ad-hoc proof does not establish Developer ID/notarization or physical-device acceptance. The normative [setup/trust contract](../../docs/product/point-guard-setup.md) defines private state and listener/admin boundaries. ## iPhone / Xcode — owner-run diff --git a/apps/purepoint-mobile/package-lock.json b/apps/purepoint-mobile/package-lock.json index d8bd06a0..7de48f20 100644 --- a/apps/purepoint-mobile/package-lock.json +++ b/apps/purepoint-mobile/package-lock.json @@ -11,7 +11,7 @@ "ws": "8.22.0" }, "devDependencies": { - "@types/node": "26.6.5", + "@types/node": "26.6.6", "@types/ws": "8.18.2", "prettier": "3.9.10", "typescript": "7.0.2" @@ -685,9 +685,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" diff --git a/apps/purepoint-mobile/package.json b/apps/purepoint-mobile/package.json index 7844e8d3..e96c658f 100644 --- a/apps/purepoint-mobile/package.json +++ b/apps/purepoint-mobile/package.json @@ -20,7 +20,7 @@ "ws": "8.22.0" }, "devDependencies": { - "@types/node": "26.6.5", + "@types/node": "26.6.6", "@types/ws": "8.18.2", "prettier": "3.9.10", "typescript": "7.0.2" diff --git a/apps/purepoint-mobile/runtime/bundled-pi-verification.md b/apps/purepoint-mobile/runtime/bundled-pi-verification.md index 81f441f3..14c134e0 100644 --- a/apps/purepoint-mobile/runtime/bundled-pi-verification.md +++ b/apps/purepoint-mobile/runtime/bundled-pi-verification.md @@ -1,6 +1,6 @@ # Bundled Pi verification — 2026-10-10 -Point Guard owns its Pi runtime. Users install and update PurePoint; they do not need to install or update Node, npm, or Pi separately. The app manifest pins Pi 1.1.0 and Node 26.12.1. The native owner launches the explicit bundled Node helper; the bridge launches that same executable with its bundled Pi adapter and package. Canonical Pi credentials and sessions remain outside the app so app updates preserve them. +Point Guard owns its Pi runtime. Users install and update PurePoint; they do not need to install or update Node, npm, or Pi separately. The app manifest pins Pi 1.1.0 and Node 26.13.0. The native owner launches the explicit bundled Node helper; the bridge launches that same executable with its bundled Pi adapter and package. Canonical Pi credentials and sessions remain outside the app so app updates preserve them. Managed RPC startup/exit recovery now directs users to Point Guard setup and PurePoint retry/update/reinstallation. Development bridge startup retains npm guidance. Missing-model feedback asks for provider/model configuration without requiring a separate terminal Pi installation. diff --git a/apps/purepoint-mobile/runtime/node-release.json b/apps/purepoint-mobile/runtime/node-release.json index 58b19f4a..401ed374 100644 --- a/apps/purepoint-mobile/runtime/node-release.json +++ b/apps/purepoint-mobile/runtime/node-release.json @@ -1,21 +1,21 @@ { - "version": "26.12.1", + "version": "26.13.0", "archives": { "arm64": { - "name": "node-v26.12.1-darwin-arm64.tar.gz", - "sha256": "1901d539912c02afa10d1cc126943a8855a7eedebe830119463470db5e6c59e8" + "name": "node-v26.13.0-darwin-arm64.tar.gz", + "sha256": "d38bd5e5a2ddce6bbb5ce45e46e208337da4d51fffe611e567fd32073d6439f6" }, "x64": { - "name": "node-v26.12.1-darwin-x64.tar.gz", - "sha256": "b85c2ea4420ad0bc4abf2dc6cf1f55130ed681f1cabf60cf3f526e4c9e2712ff" + "name": "node-v26.13.0-darwin-x64.tar.gz", + "sha256": "525c9b29bb0263d687de60a4cec0e2a6db876a8eb032369e5795eca71e7c358e" }, "linux-arm64": { - "name": "node-v26.12.1-linux-arm64.tar.gz", - "sha256": "a6c01af459dd8972ab3fb08ee5da81244d26ba8c4944a62d5bbba8c597ae154f" + "name": "node-v26.13.0-linux-arm64.tar.gz", + "sha256": "439e9c438acdf6a6ffe47418da709153d66c4bed812e30a473faacb107dd2791" }, "linux-x64": { - "name": "node-v26.12.1-linux-x64.tar.gz", - "sha256": "0fd1eb7770598f52a21995edf6ed35fc690400fa437c2386af329519e9143132" + "name": "node-v26.13.0-linux-x64.tar.gz", + "sha256": "5848553868b402d3fabb5b09270649977780a390f832155721025a08b5104455" } } } diff --git a/docs/architecture/distribution.md b/docs/architecture/distribution.md index 65cb462b..f2186af7 100644 --- a/docs/architecture/distribution.md +++ b/docs/architecture/distribution.md @@ -12,7 +12,7 @@ PurePoint needs to ship to users as a self-contained product with no external ru ! [DIST-004] Linux servers get static musl builds of `pu` and `pu-engine` (`x86_64-` and `aarch64-unknown-linux-musl`), one `purepoint--.tar.gz` (plus `.sha256`) per target, attached to the same GitHub release as the DMG. They are built natively per architecture in `release.yml`. Each tarball also gets a minisign signature (`.tar.gz.minisig`) from the release key and a GitHub build provenance attestation. Each tarball also carries the static `pu-point-guard-lock` helper that the phone access payload ships (DIST-008). `scripts/install.sh` checks the checksum, then verifies who built the download before installing anything: the signature must verify against the minisign public key embedded in `install.sh` (with `minisign`, or with OpenSSL 3 when minisign isn't installed), and when the GitHub CLI is installed and signed in, `gh attestation verify --repo 2witstudios/PurePoint` must pass. At least one of the two must succeed and any check that runs must pass, so neither check depends only on files from the same release. If neither can run (no signature on the release, or no minisign, OpenSSL 3 or `gh`), the install stops unless `PU_INSTALL_SKIP_VERIFY=1` is set, which installs with a warning. The opt-out never overrides a check that failed. A local `PU_ARCHIVE` is installed as given. The script installs both binaries side by side in `~/.pu/bin`, replacing each by rename so a running daemon's binary is never overwritten in place. There's no package-manager integration yet. -! [DIST-005] Releases are built and signed in separate jobs (`.github/workflows/release.yml`, run only by a `v*` tag push). `build-macos` and `linux` compile every third-party dependency with a read-only token, no secrets and no restored cache; the macOS app is archived ad-hoc signed. `sign-macos` runs in the protected `release` environment and is the only job that receives secrets. It runs no build code: only Apple's tools, `.github/scripts/sign-macos-app.sh` (inside-out Developer ID signing, then a check that every Mach-O in the bundle has the identity, the hardened runtime and a secure timestamp), and the Sparkle and create-dmg releases, each fetched with `curl -fsSL` and checked against a SHA-256 committed in the workflow. The Sparkle tools match the version the app links in `Package.resolved`. The certificate is imported into a temporary keychain as non-extractable and usable only by `/usr/bin/codesign`, the keychain locks after 15 idle minutes, and the keychain and `.p12` are deleted after the last signature and again in an `always()` cleanup step. `build-macos` uses the same Node (26.12.1, via `actions/setup-node`) as CI, and every `xcodebuild` that builds the app in CI or a release passes `-onlyUsePackageVersionsFromResolvedFile`, so Swift packages resolve only to the committed `Package.resolved`. Every remote Swift package is pinned to a version or a revision, never a branch. `create-dmg` failures fail the release: a non-zero exit is tolerated only when the DMG was still written and passes `hdiutil verify`, with a warning. `sign-linux` also runs in the `release` environment. It signs the Linux tarballs with `.github/scripts/minisign.sh` (minisign-format Ed25519 signatures made with OpenSSL, so no signing tool is downloaded) after checking that the `RELEASE_MINISIGN_KEY` secret matches the public key in `scripts/install.sh`. Without that secret the job skips signing with a warning and the release ships without `.minisig` files. `sbom` writes CycloneDX SBOMs for `pu` and `pu-engine` (`cargo cyclonedx --target all`, pinned version) and for the Point Guard npm runtime (`npm sbom --omit=dev`). `attest` (`id-token`, `attestations` and `artifact-metadata` write, no checkout, no `run` steps) records build provenance for the Linux tarballs and the DMG with `actions/attest`, and attests the SBOMs against them; verify with `gh attestation verify --repo 2witstudios/PurePoint`. `publish` is the only job with `contents: write` and runs only after both signing jobs and `attest` succeed. It checks out no code, creates the GitHub release with the DMG, the Linux tarballs and phone access payloads (DIST-008) and their signatures, and the SBOMs in one step, and never replaces a versioned asset. The `appcast.xml` on the `appcast` release is the moving update feed and is the one asset replaced on each release. The unsigned app and the DMG pass between jobs as workflow artifacts kept for 14 days, so a release can wait that long for environment approval and still be re-run. If the release already exists when `publish` runs again, every asset must match the published one byte for byte (by SHA-256), or the job fails before touching the appcast. Re-running every job re-signs the DMG and changes its bytes, so use "Re-run failed jobs", or delete the release first. Every action in every workflow is pinned to a full commit SHA with a version comment, and no checkout keeps the token in `.git/config`. `.github/scripts/test_release_workflow.py` checks these properties in CI, and `.github/scripts/test_install_script.py` checks the install script's verification against signatures made by `minisign.sh` (and, when `minisign` is installed, that minisign itself accepts them). +! [DIST-005] Releases are built and signed in separate jobs (`.github/workflows/release.yml`, run only by a `v*` tag push). `build-macos` and `linux` compile every third-party dependency with a read-only token, no secrets and no restored cache; the macOS app is archived ad-hoc signed. `sign-macos` runs in the protected `release` environment and is the only job that receives secrets. It runs no build code: only Apple's tools, `.github/scripts/sign-macos-app.sh` (inside-out Developer ID signing, then a check that every Mach-O in the bundle has the identity, the hardened runtime and a secure timestamp), and the Sparkle and create-dmg releases, each fetched with `curl -fsSL` and checked against a SHA-256 committed in the workflow. The Sparkle tools match the version the app links in `Package.resolved`. The certificate is imported into a temporary keychain as non-extractable and usable only by `/usr/bin/codesign`, the keychain locks after 15 idle minutes, and the keychain and `.p12` are deleted after the last signature and again in an `always()` cleanup step. `build-macos` uses the same Node (26.13.0, via `actions/setup-node`) as CI, and every `xcodebuild` that builds the app in CI or a release passes `-onlyUsePackageVersionsFromResolvedFile`, so Swift packages resolve only to the committed `Package.resolved`. Every remote Swift package is pinned to a version or a revision, never a branch. `create-dmg` failures fail the release: a non-zero exit is tolerated only when the DMG was still written and passes `hdiutil verify`, with a warning. `sign-linux` also runs in the `release` environment. It signs the Linux tarballs with `.github/scripts/minisign.sh` (minisign-format Ed25519 signatures made with OpenSSL, so no signing tool is downloaded) after checking that the `RELEASE_MINISIGN_KEY` secret matches the public key in `scripts/install.sh`. Without that secret the job skips signing with a warning and the release ships without `.minisig` files. `sbom` writes CycloneDX SBOMs for `pu` and `pu-engine` (`cargo cyclonedx --target all`, pinned version) and for the Point Guard npm runtime (`npm sbom --omit=dev`). `attest` (`id-token`, `attestations` and `artifact-metadata` write, no checkout, no `run` steps) records build provenance for the Linux tarballs and the DMG with `actions/attest`, and attests the SBOMs against them; verify with `gh attestation verify --repo 2witstudios/PurePoint`. `publish` is the only job with `contents: write` and runs only after both signing jobs and `attest` succeed. It checks out no code, creates the GitHub release with the DMG, the Linux tarballs and phone access payloads (DIST-008) and their signatures, and the SBOMs in one step, and never replaces a versioned asset. The `appcast.xml` on the `appcast` release is the moving update feed and is the one asset replaced on each release. The unsigned app and the DMG pass between jobs as workflow artifacts kept for 14 days, so a release can wait that long for environment approval and still be re-run. If the release already exists when `publish` runs again, every asset must match the published one byte for byte (by SHA-256), or the job fails before touching the appcast. Re-running every job re-signs the DMG and changes its bytes, so use "Re-run failed jobs", or delete the release first. Every action in every workflow is pinned to a full commit SHA with a version comment, and no checkout keeps the token in `.git/config`. `.github/scripts/test_release_workflow.py` checks these properties in CI, and `.github/scripts/test_install_script.py` checks the install script's verification against signatures made by `minisign.sh` (and, when `minisign` is installed, that minisign itself accepts them). ! [DIST-006] The update feed is signed. `sign-macos` generates `appcast.xml` and signs it with `sign_update` and the Sparkle EdDSA key that signs the DMG, so the feed and the update share one key. The app sets `SURequireSignedFeed` and `SUVerifyUpdateBeforeExtraction` (both need Sparkle 2.9 or later), so Sparkle rejects an unsigned or modified feed and checks the update's EdDSA signature before extracting it. With `SUVerifyUpdateBeforeExtraction`, the EdDSA key can't be rotated in an ordinary update; rotating it needs a release that turns the setting off first. Apps released before this change don't check the feed signature but still check each update's EdDSA signature and Apple team. diff --git a/docs/product/linux-phone-host.md b/docs/product/linux-phone-host.md index a57b1828..684b6656 100644 --- a/docs/product/linux-phone-host.md +++ b/docs/product/linux-phone-host.md @@ -100,7 +100,7 @@ Everything in the bridge and runtime packaging that assumes macOS or the Mac app #### [LPH-001] Node build and libc floor -Option A: official nodejs.org `linux-x64`/`linux-arm64` tarballs (glibc), the same 26.12.1 that the Mac pins. +Option A: official nodejs.org `linux-x64`/`linux-arm64` tarballs (glibc), the same 26.13.0 that the Mac pins. - Pro: the same upstream, version and SHA256SUMS flow as `node-release.json`; covers Debian/Ubuntu/Fedora/RHEL 8+, Amazon Linux 2023 and Raspberry Pi OS 64-bit. - Con: needs glibc ≥ 2.28 (Node 26's Linux build floor, unchanged from 22). Alpine/musl hosts are unsupported, unlike the static musl `pu` binaries (DIST-004).