From 26cf3b4d4e3a70fa43421fecd6442a1b39decc9d Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Sat, 1 Aug 2026 18:44:44 +0000 Subject: [PATCH] Fix fresh Windows and Linux runtime setup Signed-off-by: Joseph Yaksich --- CHANGELOG.md | 17 ++++- README.md | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/install-wsl-runtime.ps1 | 125 ++++++++++++++++++-------------- scripts/package-linux-host.mjs | 28 ++++++- site/public/install.sh | 6 +- site/public/update-host.sh | 9 ++- src/server/channel-computers.ts | 2 +- src/server/connectors.ts | 4 + src/server/db.ts | 2 +- test/channel-computers.mjs | 2 +- test/connectors.mjs | 15 ++++ test/desktop.mjs | 2 + test/site.mjs | 2 + 15 files changed, 155 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b85ca59..0861129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.33] - 2026-08-01 + +### Fixed + +- Fixed fresh Windows shared-runtime setup so the expected pre-install + `wsl.exe --version` failure is inspected instead of terminating PowerShell + before the pinned Microsoft WSL package can be downloaded and installed. +- Preserved unexpected elevated Windows host-setup failures in the shared + status file so onboarding reports the actionable cause rather than only the + child process exit code. +- Added SHA-256-pinned x64 and arm64 Cloudflare tunnel connectors to the Linux + host archive, selected the connector matching the running host, and made + fresh install/update validation reject incomplete Linux packages. + ## [0.0.32] - 2026-08-01 ### Fixed @@ -941,7 +955,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 notarization, stapled tickets, Gatekeeper verification, persistent Application Support, and isolated Apple container machines. -[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.32...HEAD +[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.33...HEAD +[0.0.33]: https://github.com/gitcommit90/1Helm/compare/v0.0.32...v0.0.33 [0.0.32]: https://github.com/gitcommit90/1Helm/compare/v0.0.31...v0.0.32 [0.0.31]: https://github.com/gitcommit90/1Helm/compare/v0.0.30...v0.0.31 [0.0.30]: https://github.com/gitcommit90/1Helm/compare/v0.0.29...v0.0.30 diff --git a/README.md b/README.md index 2e674b2..5268c38 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to | `PORT` | `8123` | HTTP/WebSocket control-plane port. | | `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and non-OCI development/Apple workspace mirrors. | | `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `oci` on Linux and Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. | -| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.32` | Versioned channel-machine image contract. | +| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.33` | Versioned channel-machine image contract. | ### Agent-first JSON CLI diff --git a/package-lock.json b/package-lock.json index dd4cba9..09a7286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "1helm", - "version": "0.0.32", + "version": "0.0.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "1helm", - "version": "0.0.32", + "version": "0.0.33", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index e8917ba..901a4e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "1helm", "productName": "1Helm", - "version": "0.0.32", + "version": "0.0.33", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/scripts/install-wsl-runtime.ps1 b/scripts/install-wsl-runtime.ps1 index 910f30f..b02bcab 100644 --- a/scripts/install-wsl-runtime.ps1 +++ b/scripts/install-wsl-runtime.ps1 @@ -37,8 +37,19 @@ function Fetch-File { # version/list matches fail unless the NULs are stripped first. function Get-WslText { param([Parameter(Mandatory = $true)][string[]]$ArgumentList) - $raw = & $wsl @ArgumentList 2>&1 | Out-String - $code = $LASTEXITCODE + # Windows PowerShell promotes native stderr to ErrorRecord objects. With the + # script-wide Stop policy, an expected nonzero probe (such as --version on a + # genuinely fresh host) otherwise aborts before we can inspect LASTEXITCODE + # and install WSL. Keep the probe non-terminating, then restore fail-closed + # behavior for the surrounding setup transaction. + $previousErrorAction = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $raw = & $wsl @ArgumentList 2>&1 | Out-String + $code = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorAction + } $text = ($raw -replace [char]0, "").Trim() return [pscustomobject]@{ ExitCode = $code; Text = $text } } @@ -103,63 +114,69 @@ function Fail-Setup { } if ($HostSetup) { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Fail-Setup "The WSL host setup phase requires administrator approval." - } - Write-SetupStatus -Status "running" -Step "Enabling Windows WSL features..." -Progress 8 - $wslFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux - $vmFeature = Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform - if ($wslFeature.State -ne "Enabled") { Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -All -NoRestart | Out-Null } - if ($vmFeature.State -ne "Enabled") { Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All -NoRestart | Out-Null } - $wslFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux - $vmFeature = Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform - $restartRequired = (Test-RestartRequired $wslFeature) -or (Test-RestartRequired $vmFeature) - $hostTemporary = Join-Path ([System.IO.Path]::GetTempPath()) ("1helm-wsl-host-" + [Guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $hostTemporary | Out-Null try { - if (-not (Test-PinnedWslRuntime)) { - $msi = Join-Path $hostTemporary "wsl.candidate.msi" - Write-SetupStatus -Status "running" -Step "Downloading Microsoft WSL installer..." -Progress 12 - Fetch-File -Url $wslInstallerUrl -Destination $msi - if ((Get-FileHash -LiteralPath $msi -Algorithm SHA256).Hash.ToLowerInvariant() -ne $wslInstallerSha256) { - Fail-Setup "Microsoft WSL installer did not match 1Helm's pinned SHA-256." - } - $signature = Get-AuthenticodeSignature -LiteralPath $msi - if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate -or - $signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') { - Fail-Setup "Microsoft WSL installer did not have a valid Microsoft Corporation signature." - } - Write-SetupStatus -Status "running" -Step "Installing Microsoft WSL $wslVersion..." -Progress 18 - $installer = Start-Process -FilePath "$env:SystemRoot\System32\msiexec.exe" -ArgumentList @("/i", $msi, "/qn", "/norestart") -Wait -PassThru - # 0 success; 1641/3010 success+reboot; 1638 already installed (same/newer). - # 1603 is a hard fail from msiexec, but on machines that already have the - # pinned WSL build (or UTF-16 made detection fail earlier) the package may - # still leave a working runtime - re-verify before failing closed. - if ($installer.ExitCode -in @(1641, 3010)) { $restartRequired = $true } - elseif ($installer.ExitCode -notin @(0, 1638)) { - if (Test-PinnedWslRuntime) { - Write-Host "Microsoft WSL installer returned $($installer.ExitCode), but pinned WSL $wslVersion is already present; continuing." - } else { - Fail-Setup "Microsoft WSL installer failed with exit code $($installer.ExitCode)." + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Fail-Setup "The WSL host setup phase requires administrator approval." + } + Write-SetupStatus -Status "running" -Step "Enabling Windows WSL features..." -Progress 8 + $wslFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux + $vmFeature = Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform + if ($wslFeature.State -ne "Enabled") { Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -All -NoRestart | Out-Null } + if ($vmFeature.State -ne "Enabled") { Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All -NoRestart | Out-Null } + $wslFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux + $vmFeature = Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform + $restartRequired = (Test-RestartRequired $wslFeature) -or (Test-RestartRequired $vmFeature) + $hostTemporary = Join-Path ([System.IO.Path]::GetTempPath()) ("1helm-wsl-host-" + [Guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $hostTemporary | Out-Null + try { + if (-not (Test-PinnedWslRuntime)) { + $msi = Join-Path $hostTemporary "wsl.candidate.msi" + Write-SetupStatus -Status "running" -Step "Downloading Microsoft WSL installer..." -Progress 12 + Fetch-File -Url $wslInstallerUrl -Destination $msi + if ((Get-FileHash -LiteralPath $msi -Algorithm SHA256).Hash.ToLowerInvariant() -ne $wslInstallerSha256) { + Fail-Setup "Microsoft WSL installer did not match 1Helm's pinned SHA-256." } + $signature = Get-AuthenticodeSignature -LiteralPath $msi + if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate -or + $signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') { + Fail-Setup "Microsoft WSL installer did not have a valid Microsoft Corporation signature." + } + Write-SetupStatus -Status "running" -Step "Installing Microsoft WSL $wslVersion..." -Progress 18 + $installer = Start-Process -FilePath "$env:SystemRoot\System32\msiexec.exe" -ArgumentList @("/i", $msi, "/qn", "/norestart") -Wait -PassThru + # 0 success; 1641/3010 success+reboot; 1638 already installed (same/newer). + # 1603 is a hard fail from msiexec, but on machines that already have the + # pinned WSL build (or UTF-16 made detection fail earlier) the package may + # still leave a working runtime - re-verify before failing closed. + if ($installer.ExitCode -in @(1641, 3010)) { $restartRequired = $true } + elseif ($installer.ExitCode -notin @(0, 1638)) { + if (Test-PinnedWslRuntime) { + Write-Host "Microsoft WSL installer returned $($installer.ExitCode), but pinned WSL $wslVersion is already present; continuing." + } else { + Fail-Setup "Microsoft WSL installer failed with exit code $($installer.ExitCode)." + } + } + } else { + Write-SetupStatus -Status "running" -Step "Microsoft WSL $wslVersion is already installed." -Progress 18 } - } else { - Write-SetupStatus -Status "running" -Step "Microsoft WSL $wslVersion is already installed." -Progress 18 - } - if ($restartRequired) { - Write-SetupStatus -Status "restart_required" -Step "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." -Progress 20 - exit 10 + if ($restartRequired) { + Write-SetupStatus -Status "restart_required" -Step "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." -Progress 20 + exit 10 + } + if (-not (Test-PinnedWslRuntime)) { Fail-Setup "Microsoft WSL $wslVersion was installed but could not be verified." } + Write-SetupStatus -Status "running" -Step "Setting WSL 2 as the default..." -Progress 22 + $defaultVersion = Get-WslText -ArgumentList @("--set-default-version", "2") + if ($defaultVersion.ExitCode -ne 0) { Fail-Setup "WSL could not set version 2 as the default. $($defaultVersion.Text)" } + } finally { + if (Test-Path -LiteralPath $hostTemporary) { Remove-Item -LiteralPath $hostTemporary -Recurse -Force } } - if (-not (Test-PinnedWslRuntime)) { Fail-Setup "Microsoft WSL $wslVersion was installed but could not be verified." } - Write-SetupStatus -Status "running" -Step "Setting WSL 2 as the default..." -Progress 22 - $defaultVersion = Get-WslText -ArgumentList @("--set-default-version", "2") - if ($defaultVersion.ExitCode -ne 0) { Fail-Setup "WSL could not set version 2 as the default. $($defaultVersion.Text)" } - } finally { - if (Test-Path -LiteralPath $hostTemporary) { Remove-Item -LiteralPath $hostTemporary -Recurse -Force } + exit 0 + } catch { + $message = $_.Exception.Message + if (-not $message) { $message = "$_" } + Fail-Setup $message } - exit 0 } # Keep the distribution owned by the signed-in Windows account. Only optional diff --git a/scripts/package-linux-host.mjs b/scripts/package-linux-host.mjs index 5a1504a..9d49956 100755 --- a/scripts/package-linux-host.mjs +++ b/scripts/package-linux-host.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; @@ -9,6 +9,19 @@ const version = String(JSON.parse(readFileSync(resolve(root, "package.json"), "u if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error("package.json must contain a release version"); const dist = resolve(root, "dist"); const output = resolve(dist, `1Helm-${version}-linux-node.tgz`); +const cloudflaredVersion = "2026.3.0"; +const cloudflared = [ + { + arch: "x64", + asset: "cloudflared-linux-amd64", + sha256: "4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30", + }, + { + arch: "arm64", + asset: "cloudflared-linux-arm64", + sha256: "0755ba4cbab59980e6148367fcf53a8f3ec85a97deefd63c2420cf7850769bee", + }, +]; const sealed = [ "container/channel-machine.oci.tar", "container/channel-machine.oci.sha256", @@ -43,6 +56,19 @@ try { copyFileSync(src, join(stage, prefix, rel)); } + const resourcesDir = join(stage, prefix, "resources"); + mkdirSync(resourcesDir, { recursive: true }); + for (const connector of cloudflared) { + const destination = join(resourcesDir, `cloudflared-linux-${connector.arch}`); + const url = `https://github.com/cloudflare/cloudflared/releases/download/${cloudflaredVersion}/${connector.asset}`; + const download = spawnSync("curl", ["-fsSL", "--proto", "=https", "--tlsv1.2", "--retry", "3", "-o", destination, url], { stdio: "inherit" }); + if (download.status !== 0) throw new Error(`Could not download pinned cloudflared for Linux ${connector.arch}`); + const digest = spawnSync("sha256sum", [destination], { encoding: "utf8" }); + const actual = digest.status === 0 ? String(digest.stdout || "").trim().split(/\s+/)[0] : ""; + if (actual !== connector.sha256) throw new Error(`Pinned cloudflared digest mismatch for Linux ${connector.arch} (got ${actual || "unavailable"})`); + chmodSync(destination, 0o755); + } + const pack = spawnSync("tar", ["-czf", output, "-C", stage, prefix], { stdio: "inherit" }); if (pack.status !== 0) throw new Error("Could not write the Linux host archive"); } finally { diff --git a/site/public/install.sh b/site/public/install.sh index efd787e..ef4f3ca 100644 --- a/site/public/install.sh +++ b/site/public/install.sh @@ -183,7 +183,8 @@ PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' " && -r "$RELEASE_STAGE/deploy/1helm-oci-runtime-v1.conf" \ && -r "$RELEASE_STAGE/container/Containerfile.oci" \ && -f "$RELEASE_STAGE/container/channel-machine.oci.tar" \ - && -f "$RELEASE_STAGE/container/channel-machine.oci.sha256" ]] \ + && -f "$RELEASE_STAGE/container/channel-machine.oci.sha256" \ + && -x "$RELEASE_STAGE/resources/cloudflared-linux-$NODE_ARCH" ]] \ || { echo "The verified Linux artifact is missing its complete OCI runtime contract." >&2; exit 1; } chown -R "$SERVICE_USER:$SERVICE_USER" "$RELEASE_STAGE" runuser -u "$SERVICE_USER" -- env HOME="$STATE_ROOT" PATH="$NODE_LINK/bin:/usr/bin:/bin" PUPPETEER_SKIP_DOWNLOAD=1 "$NODE_LINK/bin/npm" --prefix "$RELEASE_STAGE" ci @@ -193,7 +194,8 @@ if [[ -e "$RELEASE_ROOT" ]]; then EXISTING_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' "$RELEASE_ROOT/package.json" 2>/dev/null || true)" [[ "$EXISTING_VERSION" == "$VERSION" \ && -f "$RELEASE_ROOT/container/channel-machine.oci.tar" \ - && -f "$RELEASE_ROOT/container/channel-machine.oci.sha256" ]] \ + && -f "$RELEASE_ROOT/container/channel-machine.oci.sha256" \ + && -x "$RELEASE_ROOT/resources/cloudflared-linux-$NODE_ARCH" ]] \ || { echo "Existing release directory does not match the verified v$VERSION Linux artifact." >&2; exit 1; } else mv "$RELEASE_STAGE" "$RELEASE_ROOT" diff --git a/site/public/update-host.sh b/site/public/update-host.sh index 51b752c..30fb4d7 100755 --- a/site/public/update-host.sh +++ b/site/public/update-host.sh @@ -13,6 +13,11 @@ STATUS_FILE="$STATE_ROOT/host-update-status.json" LOCK_FILE="$INSTALL_ROOT/host-update.lock" SERVICE_NAME="1helm.service" PORT="8123" +case "$(uname -m)" in + x86_64|amd64) CONNECTOR_ARCH="x64" ;; + aarch64|arm64) CONNECTOR_ARCH="arm64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac HOST_CONTRACT_PATHS=( /usr/libexec/1helm-oci-runtime /etc/1helm/oci-runtime-v1.conf @@ -192,7 +197,7 @@ if [[ "$VERSION_ORDER" != "-1" ]]; then RELEASE_ROOT="$(readlink -f "$APP_ROOT" 2>/dev/null || true)" [[ "$RELEASE_ROOT" == "$RELEASES_ROOT/"* && -d "$RELEASE_ROOT" ]] \ || fail "The current 1Helm release is not inside the verified release store." - [[ -x "$RELEASE_ROOT/site/public/apply-linux-release.sh" && -x "$RELEASE_ROOT/site/public/install-oci-runtime.sh" && -x "$RELEASE_ROOT/site/public/install-linux-units.sh" && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" && -x "$RELEASE_ROOT/scripts/1helm-oci-runtime" ]] \ + [[ -x "$RELEASE_ROOT/site/public/apply-linux-release.sh" && -x "$RELEASE_ROOT/site/public/install-oci-runtime.sh" && -x "$RELEASE_ROOT/site/public/install-linux-units.sh" && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" && -x "$RELEASE_ROOT/scripts/1helm-oci-runtime" && -x "$RELEASE_ROOT/resources/cloudflared-linux-$CONNECTOR_ARCH" ]] \ || fail "The current verified release is missing its OCI runtime contract." write_status "installing" "$TARGET_VERSION" "The application is current; the host is applying its verified OCI contract." APPLY_UNIT="1helm-host-contract-apply-${TARGET_VERSION//./-}-$$" @@ -220,7 +225,7 @@ PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' " || fail "The verified Linux artifact version does not match its release tag." [[ -x "$STAGE/site/public/update-host.sh" ]] \ || fail "The verified Linux artifact is missing its host updater." -[[ -x "$STAGE/site/public/apply-linux-release.sh" && -x "$STAGE/site/public/install-oci-runtime.sh" && -x "$STAGE/site/public/install-linux-units.sh" && -x "$STAGE/site/public/uninstall-host.sh" && -x "$STAGE/scripts/1helm-oci-runtime" && -r "$STAGE/deploy/1helm-oci-runtime-v1.conf" && -r "$STAGE/container/Containerfile.oci" ]] \ +[[ -x "$STAGE/site/public/apply-linux-release.sh" && -x "$STAGE/site/public/install-oci-runtime.sh" && -x "$STAGE/site/public/install-linux-units.sh" && -x "$STAGE/site/public/uninstall-host.sh" && -x "$STAGE/scripts/1helm-oci-runtime" && -r "$STAGE/deploy/1helm-oci-runtime-v1.conf" && -r "$STAGE/container/Containerfile.oci" && -x "$STAGE/resources/cloudflared-linux-$CONNECTOR_ARCH" ]] \ || fail "The verified Linux artifact is missing its OCI runtime contract." chown -R "$SERVICE_USER:$SERVICE_USER" "$STAGE" diff --git a/src/server/channel-computers.ts b/src/server/channel-computers.ts index 66c59eb..7480b36 100644 --- a/src/server/channel-computers.ts +++ b/src/server/channel-computers.ts @@ -68,7 +68,7 @@ const APPLE_RUNTIME_VERSION = "1.1.0"; export const APPLE_RUNTIME_PACKAGE = `container-${APPLE_RUNTIME_VERSION}-installer-signed.pkg`; export const APPLE_RUNTIME_URL = `https://github.com/apple/container/releases/download/${APPLE_RUNTIME_VERSION}/${APPLE_RUNTIME_PACKAGE}`; export const APPLE_RUNTIME_SHA256 = "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"; -export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.32"; +export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.33"; const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[]; const OCI_RUNTIME_VERSION = "1helm-oci-runtime-v1"; const OCI_HELPER_CANDIDATES = [ diff --git a/src/server/connectors.ts b/src/server/connectors.ts index a50e197..1ad928f 100644 --- a/src/server/connectors.ts +++ b/src/server/connectors.ts @@ -19,6 +19,9 @@ function connectorBinary(): string { const appRoot = process.env.HELM_APP_ROOT || ""; const pathSep = process.platform === "win32" ? ";" : ":"; const pathNames = process.platform === "win32" ? ["cloudflared.exe", "cloudflared"] : ["cloudflared"]; + const linuxConnector = process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64") + ? `cloudflared-linux-${process.arch}` + : ""; const pathCandidates = String(process.env.PATH || "").split(pathSep).filter(Boolean).flatMap((directory) => pathNames.map((name) => join(directory, name))); const candidates = [ process.env.CLOUDFLARED_BIN || "", @@ -27,6 +30,7 @@ function connectorBinary(): string { resources ? join(resources, "cloudflared") : "", appRoot ? join(appRoot, "cloudflared.exe") : "", appRoot ? join(appRoot, "cloudflared") : "", + appRoot && linuxConnector ? join(appRoot, "resources", linuxConnector) : "", "/opt/homebrew/bin/cloudflared", "/usr/local/bin/cloudflared", "/usr/bin/cloudflared", diff --git a/src/server/db.ts b/src/server/db.ts index c661723..ca99041 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -939,7 +939,7 @@ export function migrate(): void { const platformBackend = process.platform === "darwin" ? "apple" : "oci"; const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend); const backend = ["apple", "oci", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend; - const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.32"); + const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.33"); for (const channel of q(`SELECT c.id FROM channels c JOIN agent_channels ac ON ac.channel_id=c.id WHERE c.kind='channel' AND c.status<>'deleted'`)) { const channelId = Number(channel.id); diff --git a/test/channel-computers.mjs b/test/channel-computers.mjs index e50442e..984c330 100644 --- a/test/channel-computers.mjs +++ b/test/channel-computers.mjs @@ -181,7 +181,7 @@ test("Apple channel-computer contract preserves isolation, files, wakes, archive test("runtime digest and packaged image recipe stay pinned", async () => { assert.equal(computers.APPLE_RUNTIME_SHA256, "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"); assert.match(computers.APPLE_RUNTIME_URL, /\/1\.1\.0\/container-1\.1\.0-installer-signed\.pkg$/); - assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.32"); + assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.33"); const packaging = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8"); assert.match(packaging, /container\(\?:\$\|\\\/\)/, "release packaging includes container/ image assets"); const image = await readFile(join(root, "container", "Containerfile"), "utf8"); diff --git a/test/connectors.mjs b/test/connectors.mjs index c3e87db..5fc15e1 100644 --- a/test/connectors.mjs +++ b/test/connectors.mjs @@ -1,9 +1,24 @@ import assert from "node:assert/strict"; import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; +test("Linux release packaging ships pinned connectors for every supported host architecture", () => { + const packageLinux = readFileSync(join(import.meta.dirname, "..", "scripts", "package-linux-host.mjs"), "utf8"); + const resolver = readFileSync(join(import.meta.dirname, "..", "src", "server", "connectors.ts"), "utf8"); + for (const [arch, asset, digest] of [ + ["x64", "cloudflared-linux-amd64", "4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30"], + ["arm64", "cloudflared-linux-arm64", "0755ba4cbab59980e6148367fcf53a8f3ec85a97deefd63c2420cf7850769bee"], + ]) { + assert.match(packageLinux, new RegExp(`arch: "${arch}"[\\s\\S]*asset: "${asset}"[\\s\\S]*sha256: "${digest}"`)); + } + assert.match(packageLinux, /cloudflared-linux-\$\{connector\.arch\}/, "the verified binaries enter the Linux release archive"); + assert.match(packageLinux, /chmodSync\(destination, 0o755\)/, "packaged Linux connectors retain executable mode"); + assert.match(resolver, /cloudflared-linux-\$\{process\.arch\}/, "Linux resolves only the binary matching the running host architecture"); +}); + test("stopping a connector cancels automatic relaunch while preserving its credentials", async (t) => { const root = await mkdtemp(join(tmpdir(), "1helm-connector-test-")); const binary = join(root, "cloudflared"); diff --git a/test/desktop.mjs b/test/desktop.mjs index ff06e66..0d29cb6 100644 --- a/test/desktop.mjs +++ b/test/desktop.mjs @@ -189,7 +189,9 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn assert.match(windowsRuntime, /Write-SetupStatus/, "Windows setup writes machine-readable progress for the app UI"); assert.match(windowsRuntime, /\$null -eq \$hostProcess -or \$null -eq \$hostProcess\.ExitCode/, "cancelled UAC is reported as setup failure instead of a silent no-op"); assert.match(windowsRuntime, /Get-WslText|replace \[char\]0/, "WSL UTF-16 NUL output is normalized before version and distro matching"); + assert.match(windowsRuntime, /function Get-WslText[\s\S]*ErrorActionPreference = "Continue"[\s\S]*\$LASTEXITCODE[\s\S]*ErrorActionPreference = \$previousErrorAction/, "a fresh host's expected failing WSL probe cannot terminate setup before the pinned runtime is installed"); assert.match(windowsRuntime, /Test-PinnedWslRuntime/, "host setup verifies the pinned Microsoft WSL build"); + assert.match(windowsRuntime, /if \(\$HostSetup\)[\s\S]*try \{[\s\S]*catch \{[\s\S]*Fail-Setup \$message/, "unexpected elevated host-setup errors are written to shared status instead of collapsing to an unexplained exit code"); assert.match(windowsRuntime, /StatusPath/, "elevated HostSetup receives the shared status path so real errors reach the app"); assert.match(windowsRuntime, /1603[\s\S]*Test-PinnedWslRuntime|Test-PinnedWslRuntime[\s\S]*1603/, "MSI 1603 falls back to re-verifying an already-present pinned WSL runtime"); assert.match(windowsRuntime, /\[automount\][\s\S]*enabled=false[\s\S]*\[interop\][\s\S]*enabled=false/, "the shared runtime exposes neither Windows drives nor process interop"); diff --git a/test/site.mjs b/test/site.mjs index 5807a91..0972584 100644 --- a/test/site.mjs +++ b/test/site.mjs @@ -193,6 +193,7 @@ test("installer assets are explicit and syntax-valid", () => { assert.match(installer, /expectedUrl = `https:\/\/github\.com\/gitcommit90\/1Helm\/releases\/download\/v\$\{version\}\/\$\{name\}`/, "fresh installs accept only the canonical artifact URL for the resolved version"); assert.match(installer, /RELEASE_SHA256[\s\S]*sha256sum -c -[\s\S]*tar -xzf/, "fresh installs verify the Linux release digest before extraction"); assert.match(installer, /install-oci-runtime\.sh[\s\S]*channel-machine\.oci\.tar[\s\S]*npm[^\n]*ci/, "fresh installs reject an artifact without the complete OCI runtime before running release code"); + assert.match(installer, /resources\/cloudflared-linux-\$NODE_ARCH/, "fresh Linux installs reject archives without the connector for the current architecture"); assert.match(installer, /NETWORK_BACKEND_FILE[\s\S]*cat "\$NETWORK_BACKEND_FILE"[\s\S]*printf '%s' netavark[\s\S]*install-oci-runtime\.sh/, "the web bootstrap repairs v0.0.30's newline-terminated Podman backend before invoking release code"); assert.doesNotMatch(installer, /git clone|git checkout/, "fresh installs never combine the current installer with an older source-only tag"); assert.doesNotMatch(installer, /api\.github\.com/, "fresh installs do not depend on unauthenticated GitHub API quota"); @@ -213,6 +214,7 @@ test("installer assets are explicit and syntax-valid", () => { assert.match(updater, /browser_download_url/); assert.match(updater, /\^sha256:\[a-f0-9\]\{64\}\$/, "the Linux updater requires GitHub's exact SHA-256 asset digest"); assert.match(updater, /sha256sum -c -/); + assert.match(updater, /CONNECTOR_ARCH[\s\S]*resources\/cloudflared-linux-\$CONNECTOR_ARCH/, "Linux updates reject archives without the connector for the current architecture"); assert.match(updater, /mv -Tf .*current/); assert.match(updater, /atomic host transaction[\s\S]*rollback was proven healthy/i, "the updater defers rollback reporting to the transaction that can prove restored HTTP health"); assert.match(updater, /VERSION_ORDER[\s\S]*Never downgrade[\s\S]*TARGET_VERSION="\$CURRENT_VERSION"/, "the root updater never replaces a newer installed host with an older latest-release response");