From 86a668b30e7a9c49efb9c1078a36c6884e6a403d Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Sat, 1 Aug 2026 03:24:05 +0000 Subject: [PATCH] Fix fresh Linux installation from the website Signed-off-by: Joseph Yaksich --- CHANGELOG.md | 12 ++++- README.md | 12 ++--- docs/USER_GUIDE.md | 2 +- docs/VISION.md | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/1helm-oci-runtime | 11 ++++- site/content.mjs | 2 +- site/public/install.sh | 78 ++++++++++++++++++++++++++------- site/server.mjs | 66 +++++++++++++++++++++++----- src/server/channel-computers.ts | 2 +- src/server/db.ts | 2 +- test/channel-computers.mjs | 2 +- test/site.mjs | 38 ++++++++++++++-- 14 files changed, 188 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4cf012..f0afe92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.31] - 2026-08-01 + +### Fixed + +- Fixed fresh Linux installation from `1helm.com`: the public installer now + downloads and SHA-256-verifies the accepted Linux host artifact instead of + pairing its current OCI setup logic with the obsolete source-only v0.0.28 + tag, which lacked the OCI runtime installer and sealed channel image. + ### Documentation - Updated the README, standalone website, manual, story, and user guide for the @@ -899,7 +908,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.30...HEAD +[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.31...HEAD +[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 [0.0.29]: https://github.com/gitcommit90/1Helm/compare/v0.0.28...v0.0.29 [0.0.28]: https://github.com/gitcommit90/1Helm/compare/v0.0.27...v0.0.28 diff --git a/README.md b/README.md index aec6167..17507a5 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Mac, Linux, and Windows use one synchronized desktop release version. A release is held in full until the signed/notarized Mac DMG and updater ZIP, verified Linux host archive, and Windows Setup/Squirrel feed have all passed native install and update acceptance from the same source commit. Windows -Authenticode status is disclosed in every release; v0.0.30 is `NotSigned`. +Authenticode status is disclosed in every release; v0.0.31 is `NotSigned`. ### Connect from a phone or tablet @@ -152,10 +152,10 @@ frontend; the password is never retained and the resulting session is stored in the iOS Keychain or encrypted with a key held by Android Keystore. - The native iOS and Android gateway source is included in this repository, - but neither mobile platform has a current v0.0.30 public build. The most + but neither mobile platform has a current v0.0.31 public build. The most recent signed Android APK is the older v0.0.23 gateway, and 1Helm is not currently listed in the public iOS App Store. Use the HTTPS browser interface - for the current v0.0.30 experience. + for the current v0.0.31 experience. - The native clients require HTTPS, do not contain or initialize the 1Helm server or a frozen copy of its product frontend, and do not retain host data or provider credentials beyond the selected server address and secure @@ -277,7 +277,7 @@ and an audit trail. A prompt saying “use this service” is not a connector. service with health-check rollback. - Signed, Apple-notarized, stapled Apple Silicon DMG releases. - Browser access from phones and tablets to an already configured HTTPS 1Helm - host; native mobile gateway source is present but has no v0.0.30 public build. + host; native mobile gateway source is present but has no v0.0.31 public build. ### Platform truth @@ -286,7 +286,7 @@ and an audit trail. A prompt saying “use this service” is not a connector. | **Apple Silicon macOS 26** | Native desktop product and real isolated Linux computer per resident (Apple `container machine`, `home-mount=none`). | | **Linux / CI** | Supported headless systemd host with one durable Podman OCI container per resident, runtime-owned storage, and exact ownership checks; CI may select an explicit test backend. | | **Windows 11 x64** | Native desktop product with one installation-scoped WSL 2 OCI runtime and one durable container per resident; Windows-drive mounts and interop are disabled. | -| **iPhone, iPad, and Android** | Use the current HTTPS browser interface. Native gateway source exists, but v0.0.30 has no public mobile artifact and the iOS app is not publicly listed. | +| **iPhone, iPad, and Android** | Use the current HTTPS browser interface. Native gateway source exists, but v0.0.31 has no public mobile artifact and the iOS app is not publicly listed. | Not yet shipped: current public mobile builds, a native Linux desktop shell, a hosted control plane, rich Photon attachment fidelity, or blind execution of @@ -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.30` | Versioned channel-machine image contract. | +| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.31` | Versioned channel-machine image contract. | ### Agent-first JSON CLI diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 4a3cb3e..8d77feb 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -445,7 +445,7 @@ fallback. - Operational history is locally tamper-evident. - Mac artifacts are Developer ID signed, notarized, stapled, and Gatekeeper verified. Linux assets are digest-verified, and Windows Authenticode status - is disclosed for every release (`NotSigned` for v0.0.30). + is disclosed for every release (`NotSigned` for v0.0.31). For the detailed boundary, see [SECURITY.md](../SECURITY.md). For product intent, see [VISION.md](VISION.md). diff --git a/docs/VISION.md b/docs/VISION.md index 1005173..7f890fc 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -91,7 +91,7 @@ verification. synchronized public desktop-host product. Every named desktop release must publish all three from one version and exact source commit. - 1Helm is self-hosted and open source. Native mobile gateway source is present, - but v0.0.30 has no public Android/iOS build and 1Helm is not listed in the + but v0.0.31 has no public Android/iOS build and 1Helm is not listed in the public iOS App Store. A hosted control plane, blind community-skill execution, and a native Linux desktop shell are not shipped. diff --git a/package-lock.json b/package-lock.json index bfc5a91..1168c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "1helm", - "version": "0.0.30", + "version": "0.0.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "1helm", - "version": "0.0.30", + "version": "0.0.31", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index 5e28b3c..b5bc4a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "1helm", "productName": "1Helm", - "version": "0.0.30", + "version": "0.0.31", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/scripts/1helm-oci-runtime b/scripts/1helm-oci-runtime index 98834f0..1158f7d 100755 --- a/scripts/1helm-oci-runtime +++ b/scripts/1helm-oci-runtime @@ -80,9 +80,16 @@ ensure_roots() { # Prefer netavark on a fresh dedicated store. CNI writes IPAM state beneath # /var/lib/cni, which is intentionally read-only to the hardened service; # netavark keeps its runtime state under the explicit Podman runroot. - if [[ ! -e "$STORAGE_ROOT/defaultNetworkBackend" ]] \ + if [[ -f "$STORAGE_ROOT/defaultNetworkBackend" ]] \ + && [[ "$(cat "$STORAGE_ROOT/defaultNetworkBackend")" == netavark ]]; then + # Repair selectors written by v0.0.30 as well as creating new ones below. + printf '%s' netavark >"$STORAGE_ROOT/defaultNetworkBackend" + chmod 0600 "$STORAGE_ROOT/defaultNetworkBackend" + elif [[ ! -e "$STORAGE_ROOT/defaultNetworkBackend" ]] \ && { command -v netavark >/dev/null 2>&1 || [[ -x /usr/lib/podman/netavark || -x /usr/libexec/podman/netavark ]]; }; then - printf 'netavark\n' >"$STORAGE_ROOT/defaultNetworkBackend" + # Podman reads this as an exact token. Ubuntu 24.04's Podman rejects the + # newline that ordinary text-file conventions would otherwise add. + printf '%s' netavark >"$STORAGE_ROOT/defaultNetworkBackend" chmod 0600 "$STORAGE_ROOT/defaultNetworkBackend" fi } diff --git a/site/content.mjs b/site/content.mjs index 0c790e9..c9bb2ad 100644 --- a/site/content.mjs +++ b/site/content.mjs @@ -13,7 +13,7 @@ const doc = (path, title, description, content) => ({ title, description, kind: const security = doc("/manual/security-model", "Security model", "How 1Helm isolates residents, brokers credentials, audits actions, validates skills, and defines the human boundary.", `

Autonomy without architecture is just ambient authority. 1Helm makes routine action cheap inside a narrow world and makes boundary crossings explicit, attributable, and recoverable.

Resident isolation

Each ordinary channel receives a separate persistent Linux world: an Apple container machine with no Mac home mount, or a durable OCI container. Linux runs OCI natively. Windows hosts containers inside one managed WSL 2 runtime whose Windows-drive mounts and interop are disabled. Exact labels, storage mounts, and owner markers gate lifecycle operations. Other residents and the host home are not exposed.

Authoritative files

OCI workspace storage belongs to the runtime and is authoritative. Files and Cowork receive narrow direct access to that channel's storage; command and terminal paths do not copy the whole workspace. Apple's backend retains its bounded, symlink-contained mirror.

Skipper boundary

Skipper owns native host operations, fleet lifecycle, credential brokering, and cross-channel work. A resident calls Skipper directly with the invoking thread; a Captain-authored request is required for host-authorized operations. Skipper returns the result to the resident automatically.

Credentials and connections

Provider, Gmail, and Photon credentials stay in host-owned storage. Residents receive task-scoped tools and permission records, not raw access tokens or the native Messages database. Photon accepts only the configured Captain phone and keeps that direct Skipper conversation in the Captain's private #main.

Skill supply chain

The external catalog is discovery metadata, not executable trust. 1Helm shows the open registry's results without applying its own browse-time allowlist. A selected GitHub source is resolved to an immutable commit, bounded to 256 KiB, scanned for instruction override, exfiltration, remote-pipe execution, broad destructive commands, security disabling, private-host access, and prompt extraction, then hashed and wrapped beneath runtime authority.

Audit and limits

New activity, tool starts/results, and skill installation decisions enter an append-only SHA-256 chain. The chain is tamper-evident, not a remote transparency log: an administrator with database access can still delete or replace the entire database. Historical rows predating the chain are not backfilled.

Known dependency debt

The pinned Photon SDK currently carries moderate OpenTelemetry advisories upstream. It runs in a supervised loopback-only child process with telemetry disabled. 1Helm tracks the exact pin and will upgrade when the required Photon API remains compatible; this is not represented as a clean dependency audit.

Report a vulnerability

Use GitHub's private vulnerability reporting for the 1Helm repository. Do not open a public issue containing credentials, tokens, or an unpatched exploit.

`); -const gettingStarted = doc("/manual/getting-started", "Getting started", "Install 1Helm, connect providers, create the workspace, and give the first resident a real outcome.", `

The normal setup is three product decisions. 1Helm handles the infrastructure around them.

1. Install or connect

On Apple Silicon, download the signed, notarized, and stapled DMG. On Windows 11 x64, download the Setup executable; its Authenticode status is disclosed in the release notes and v0.0.30 is NotSigned. Ubuntu/Debian hosts use the digest-verified Linux systemd installer. A new desktop installation can host its own workspace or connect to an existing HTTPS 1Helm host. Native mobile apps connect only to an existing configured host. Starting a new host may request one administrator approval for its isolated Linux runtime.

2. Captain

Create the first account. This is the Captain: owner, final authority, and administrator. Public registration closes after the Captain exists.

3. Providers

Connect one or more subscription accounts or API keys. You can add more later, pool accounts, select exact models, and build fallback or round-robin routes. There is no required single “AI brain.”

4. Workspace

Name the workspace. Terminals default on. 1Helm creates #main with the one Skipper, then you create ordinary channels with plain-language purposes. Every ordinary channel gets a private Linux computer.

5. Give an outcome

Try: “Audit this launch folder, turn the notes into a decision brief, resolve obvious gaps yourself, and give me the finished PDF with evidence.” The resident should inspect, execute, create the artifact, and call Skipper itself if it crosses the channel boundary.

`); +const gettingStarted = doc("/manual/getting-started", "Getting started", "Install 1Helm, connect providers, create the workspace, and give the first resident a real outcome.", `

The normal setup is three product decisions. 1Helm handles the infrastructure around them.

1. Install or connect

On Apple Silicon, download the signed, notarized, and stapled DMG. On Windows 11 x64, download the Setup executable; its Authenticode status is disclosed in the release notes and v0.0.31 is NotSigned. Ubuntu/Debian hosts use the digest-verified Linux systemd installer. A new desktop installation can host its own workspace or connect to an existing HTTPS 1Helm host. Native mobile apps connect only to an existing configured host. Starting a new host may request one administrator approval for its isolated Linux runtime.

2. Captain

Create the first account. This is the Captain: owner, final authority, and administrator. Public registration closes after the Captain exists.

3. Providers

Connect one or more subscription accounts or API keys. You can add more later, pool accounts, select exact models, and build fallback or round-robin routes. There is no required single “AI brain.”

4. Workspace

Name the workspace. Terminals default on. 1Helm creates #main with the one Skipper, then you create ordinary channels with plain-language purposes. Every ordinary channel gets a private Linux computer.

5. Give an outcome

Try: “Audit this launch folder, turn the notes into a decision brief, resolve obvious gaps yourself, and give me the finished PDF with evidence.” The resident should inspect, execute, create the artifact, and call Skipper itself if it crosses the channel boundary.

`); const architecture = doc("/manual/architecture", "Architecture", "The 1Helm control plane, resident computers, Skipper, model fabric, memory, obligations, connections, and audit chain.", `

1Helm is a compact local control plane around many persistent employee worlds.

Captain
   └─ #main / Skipper ── host · credentials · fleet · cross-channel
        ├─ #product / resident ── private Linux computer ── /workspace
diff --git a/site/public/install.sh b/site/public/install.sh
index 5e31848..efd787e 100644
--- a/site/public/install.sh
+++ b/site/public/install.sh
@@ -1,7 +1,6 @@
 #!/usr/bin/env bash
 set -euo pipefail
 
-REPO="https://github.com/gitcommit90/1Helm.git"
 INSTALL_ROOT="/opt/1helm"
 RELEASES_ROOT="$INSTALL_ROOT/releases"
 APP_ROOT="$INSTALL_ROOT/current"
@@ -10,7 +9,7 @@ NODE_LINK="$INSTALL_ROOT/node-current"
 STATE_ROOT="/var/lib/1helm-oci-v1"
 SERVICE_USER="1helm"
 NODE_VERSION="22.23.1"
-RELEASE_VERSION="0.0.28"
+RELEASE_METADATA_URL="https://1helm.com/api/releases/linux/latest"
 HOST_CONTRACT_PATHS=(
   /usr/libexec/1helm-oci-runtime
   /etc/1helm/oci-runtime-v1.conf
@@ -47,12 +46,12 @@ case "$(uname -m)" in
   *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
 esac
 
-need=(curl git tar xz sha256sum flock make c++ python3 podman crun fuse-overlayfs setfacl getfacl sudo visudo)
+need=(curl tar xz sha256sum flock make c++ python3 podman crun fuse-overlayfs setfacl getfacl sudo visudo)
 missing=()
 for command in "${need[@]}"; do command -v "$command" >/dev/null || missing+=("$command"); done
 if ((${#missing[@]})) || ! python3 -c 'import ensurepip' >/dev/null 2>&1; then
   apt-get update
-  DEBIAN_FRONTEND=noninteractive apt-get install -y curl git xz-utils ca-certificates util-linux build-essential python3 \
+  DEBIAN_FRONTEND=noninteractive apt-get install -y curl xz-utils ca-certificates util-linux build-essential python3 \
     python3-venv acl aardvark-dns crun fuse-overlayfs netavark podman uidmap sudo rsync
 fi
 
@@ -146,19 +145,58 @@ fi
 ln -sfn "$NODE_RELEASE" "$TEMP_ROOT/node-current"
 mv -Tf "$TEMP_ROOT/node-current" "$NODE_LINK"
 
-VERSION="$RELEASE_VERSION"
-git clone --depth 1 --branch "v$VERSION" "$REPO" "$TEMP_ROOT/source"
-RELEASE_SHA="$(git -C "$TEMP_ROOT/source" rev-parse HEAD)"
-[[ "$RELEASE_SHA" =~ ^[a-f0-9]{40}$ ]] || { echo "Could not resolve the checked-out release commit." >&2; exit 1; }
-RELEASE_ROOT="$RELEASES_ROOT/$VERSION-${RELEASE_SHA:0:12}"
-chown -R "$SERVICE_USER:$SERVICE_USER" "$TEMP_ROOT/source"
-runuser -u "$SERVICE_USER" -- env HOME="$STATE_ROOT" PATH="$NODE_LINK/bin:/usr/bin:/bin" PUPPETEER_SKIP_DOWNLOAD=1 "$NODE_LINK/bin/npm" --prefix "$TEMP_ROOT/source" ci
-runuser -u "$SERVICE_USER" -- env HOME="$STATE_ROOT" PATH="$NODE_LINK/bin:/usr/bin:/bin" "$NODE_LINK/bin/npm" --prefix "$TEMP_ROOT/source" run build
+curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -o "$TEMP_ROOT/release.json" "$RELEASE_METADATA_URL"
+RELEASE_OUTPUT="$("$NODE_LINK/bin/node" - "$TEMP_ROOT/release.json" <<'NODE'
+const fs = require("node:fs");
+const release = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
+const version = String(release.version || "");
+const url = String(release.url || "");
+const sha256 = String(release.sha256 || "");
+const name = `1Helm-${version}-linux-node.tgz`;
+const expectedUrl = `https://github.com/gitcommit90/1Helm/releases/download/v${version}/${name}`;
+if (!/^\d+\.\d+\.\d+$/.test(version) || url !== expectedUrl || !/^[a-f0-9]{64}$/.test(sha256)) process.exit(2);
+console.log(version);
+console.log(url);
+console.log(sha256);
+NODE
+)" || { echo "1helm.com did not return a complete stable Linux release." >&2; exit 1; }
+mapfile -t RELEASE <<<"$RELEASE_OUTPUT"
+[[ "${#RELEASE[@]}" -eq 3 ]] || { echo "1helm.com returned incomplete Linux release metadata." >&2; exit 1; }
+VERSION="${RELEASE[0]}"
+RELEASE_URL="${RELEASE[1]}"
+RELEASE_SHA256="${RELEASE[2]}"
+RELEASE_ARCHIVE="$TEMP_ROOT/1Helm-$VERSION-linux-node.tgz"
+curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -o "$RELEASE_ARCHIVE" "$RELEASE_URL"
+printf '%s  %s\n' "$RELEASE_SHA256" "$(basename "$RELEASE_ARCHIVE")" \
+  | (cd "$TEMP_ROOT" && sha256sum -c -)
+
+RELEASE_STAGE="$TEMP_ROOT/source"
+install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0750 "$RELEASE_STAGE"
+tar -xzf "$RELEASE_ARCHIVE" -C "$RELEASE_STAGE" --strip-components=1
+PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' "$RELEASE_STAGE/package.json" 2>/dev/null || true)"
+[[ "$PACKAGE_VERSION" == "$VERSION" ]] || { echo "The verified Linux artifact version does not match v$VERSION." >&2; exit 1; }
+[[ -x "$RELEASE_STAGE/site/public/apply-linux-release.sh" \
+   && -x "$RELEASE_STAGE/site/public/install-oci-runtime.sh" \
+   && -x "$RELEASE_STAGE/site/public/install-linux-units.sh" \
+   && -x "$RELEASE_STAGE/site/public/uninstall-host.sh" \
+   && -x "$RELEASE_STAGE/scripts/1helm-oci-runtime" \
+   && -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" ]] \
+  || { 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
+runuser -u "$SERVICE_USER" -- env HOME="$STATE_ROOT" PATH="$NODE_LINK/bin:/usr/bin:/bin" "$NODE_LINK/bin/npm" --prefix "$RELEASE_STAGE" run build
+RELEASE_ROOT="$RELEASES_ROOT/$VERSION-$RELEASE_SHA256"
 if [[ -e "$RELEASE_ROOT" ]]; then
-  EXISTING_SHA="$(runuser -u "$SERVICE_USER" -- git -C "$RELEASE_ROOT" rev-parse HEAD 2>/dev/null || true)"
-  [[ "$EXISTING_SHA" == "$RELEASE_SHA" ]] || { echo "Existing release directory does not match v$VERSION." >&2; exit 1; }
+  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" ]] \
+    || { echo "Existing release directory does not match the verified v$VERSION Linux artifact." >&2; exit 1; }
 else
-  mv "$TEMP_ROOT/source" "$RELEASE_ROOT"
+  mv "$RELEASE_STAGE" "$RELEASE_ROOT"
 fi
 # The application owns the top-level state directory, while the OCI helper
 # deliberately owns its persistent runtime subtree as root.  A repeat install
@@ -168,6 +206,16 @@ PREVIOUS_RELEASE="$(readlink -f "$APP_ROOT" 2>/dev/null || true)"
 [[ "$PREVIOUS_RELEASE" == "$RELEASES_ROOT/"* && -d "$PREVIOUS_RELEASE" ]] || PREVIOUS_RELEASE=""
 snapshot_host_contract
 TRANSACTION_ACTIVE=1
+# v0.0.30's otherwise accepted Linux artifact wrote this Podman selector with
+# a trailing newline. Fresh Ubuntu 24.04 rejects that byte sequence, so the
+# bootstrap repairs it before invoking either that helper or a newer one. An
+# existing non-netavark selection remains untouched.
+NETWORK_BACKEND_FILE="$STATE_ROOT/runtime/oci/storage/defaultNetworkBackend"
+if [[ ! -e "$NETWORK_BACKEND_FILE" || (-f "$NETWORK_BACKEND_FILE" && "$(cat "$NETWORK_BACKEND_FILE")" == netavark) ]]; then
+  install -d -o root -g root -m 0700 "$(dirname "$NETWORK_BACKEND_FILE")"
+  printf '%s' netavark >"$NETWORK_BACKEND_FILE"
+  chmod 0600 "$NETWORK_BACKEND_FILE"
+fi
 "$RELEASE_ROOT/site/public/install-oci-runtime.sh" "$RELEASE_ROOT"
 
 ln -s "$RELEASE_ROOT" "$TEMP_ROOT/current"
diff --git a/site/server.mjs b/site/server.mjs
index dfddd95..f13e0b6 100644
--- a/site/server.mjs
+++ b/site/server.mjs
@@ -36,25 +36,57 @@ const FEEDBACK_BODY_LIMIT = 15 * 1024 * 1024;
 const FEEDBACK_RATE_LIMIT = 30;
 const FEEDBACK_RATE_WINDOW_MS = 60_000;
 
-let releaseCache = { at: 0, assets: null };
+let releaseCache = { at: 0, release: null };
 let feedbackDatabase;
 const feedbackRate = new Map();
+async function latestRelease() {
+  if (Date.now() - releaseCache.at < RELEASE_CACHE_MS && releaseCache.release) return releaseCache.release;
+  const releaseOverride = String(process.env.SITE_RELEASE_METADATA_JSON || "");
+  let release;
+  if (releaseOverride) {
+    release = JSON.parse(releaseOverride);
+  } else {
+    const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
+      headers: { "user-agent": "1helm-site", accept: "application/vnd.github+json" },
+      signal: AbortSignal.timeout(8000),
+    });
+    if (!response.ok) throw new Error(`GitHub API ${response.status}`);
+    release = await response.json();
+  }
+  const version = String(release.tag_name || "").replace(/^v/, "");
+  if (!/^\d+\.\d+\.\d+$/.test(version) || release.draft || release.prerelease) throw new Error("latest release is not stable");
+  releaseCache = { at: Date.now(), release };
+  return release;
+}
 async function latestReleaseAssets() {
-  if (Date.now() - releaseCache.at < RELEASE_CACHE_MS && releaseCache.assets) return releaseCache.assets;
-  const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
-    headers: { "user-agent": "1helm-site", accept: "application/vnd.github+json" },
-    signal: AbortSignal.timeout(8000),
-  });
-  if (!response.ok) throw new Error(`GitHub API ${response.status}`);
-  const release = await response.json();
-  releaseCache = { at: Date.now(), assets: release.assets || [] };
-  return releaseCache.assets;
+  return (await latestRelease()).assets || [];
 }
 async function latestAssetUrl(pattern) {
   const asset = (await latestReleaseAssets()).find((entry) => pattern.test(String(entry.name || "")));
   if (!asset?.browser_download_url) throw new Error("no matching asset on latest release");
   return asset.browser_download_url;
 }
+async function latestLinuxRelease() {
+  const release = await latestRelease();
+  const version = String(release.tag_name).replace(/^v/, "");
+  const expectedNames = [
+    `1Helm-${version}-arm64.dmg`,
+    `1Helm-${version}-mac-arm64.zip`,
+    `1Helm-${version}-linux-node.tgz`,
+    `1Helm-${version}-windows-x64-setup.exe`,
+    `1Helm-${version}-full.nupkg`,
+    "RELEASES",
+  ];
+  const assets = Array.isArray(release.assets) ? release.assets : [];
+  const matrix = expectedNames.map((name) => assets.find((asset) => asset.name === name));
+  if (matrix.some((asset) => !asset || !/^sha256:[a-f0-9]{64}$/.test(String(asset.digest || "")))) {
+    throw new Error("latest release does not contain the complete digest-qualified desktop matrix");
+  }
+  const linux = matrix[2];
+  const expectedUrl = `https://github.com/${REPO}/releases/download/v${version}/${linux.name}`;
+  if (linux.browser_download_url !== expectedUrl) throw new Error("latest Linux release URL does not match its version");
+  return { version, url: expectedUrl, sha256: linux.digest.slice(7) };
+}
 
 const mime = {
   ".css": "text/css; charset=utf-8",
@@ -316,6 +348,20 @@ const server = createServer(async (req, res) => {
     });
     return;
   }
+  if (path === "/api/releases/linux/latest") {
+    try {
+      answer(res, 200, JSON.stringify(await latestLinuxRelease()), {
+        "content-type": "application/json; charset=utf-8",
+        "cache-control": "no-store",
+      });
+    } catch {
+      answer(res, 503, JSON.stringify({ error: "A complete stable Linux release is not available." }), {
+        "content-type": "application/json; charset=utf-8",
+        "cache-control": "no-store",
+      });
+    }
+    return;
+  }
   if (STATIC_PAGES[path]) {
     answer(res, 200, STATIC_PAGES[path], {
       "content-type": "text/html; charset=utf-8",
diff --git a/src/server/channel-computers.ts b/src/server/channel-computers.ts
index 2d137e9..9516a9e 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.30";
+export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.31";
 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/db.ts b/src/server/db.ts
index 2f43e26..b28143d 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.30");
+    const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.31");
     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 741aef8..0eb668d 100644
--- a/test/channel-computers.mjs
+++ b/test/channel-computers.mjs
@@ -167,7 +167,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.30");
+  assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.31");
   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/site.mjs b/test/site.mjs
index 0bcee91..fde4fb0 100644
--- a/test/site.mjs
+++ b/test/site.mjs
@@ -9,6 +9,23 @@ import { DatabaseSync } from "node:sqlite";
 import test from "node:test";
 
 const root = new URL("..", import.meta.url).pathname;
+const releaseFixture = {
+  tag_name: "v0.0.31",
+  draft: false,
+  prerelease: false,
+  assets: [
+    ["1Helm-0.0.31-arm64.dmg", "a"],
+    ["1Helm-0.0.31-mac-arm64.zip", "b"],
+    ["1Helm-0.0.31-linux-node.tgz", "c"],
+    ["1Helm-0.0.31-windows-x64-setup.exe", "d"],
+    ["1Helm-0.0.31-full.nupkg", "e"],
+    ["RELEASES", "f"],
+  ].map(([name, digit]) => ({
+    name,
+    digest: `sha256:${digit.repeat(64)}`,
+    browser_download_url: `https://github.com/gitcommit90/1Helm/releases/download/v0.0.31/${name}`,
+  })),
+};
 const freePort = () => new Promise((resolve, reject) => { const server = createServer(); server.once("error", reject); server.listen(0, "127.0.0.1", () => { const port = server.address().port; server.close(() => resolve(port)); }); });
 const waitFor = async (url) => { const deadline = Date.now() + 10_000; while (Date.now() < deadline) { try { const result = await fetch(url); if (result.ok) return result; } catch {} await new Promise((resolve) => setTimeout(resolve, 80)); } throw new Error(`Timed out: ${url}`); };
 const requestWithHost = (port, path, host) => new Promise((resolve, reject) => {
@@ -22,7 +39,7 @@ const requestWithHost = (port, path, host) => new Promise((resolve, reject) => {
 
 test("standalone 1helm.com website serves independent product and documentation surface", async () => {
   const port = await freePort();
-  const child = spawn(process.execPath, ["site/server.mjs"], { cwd: root, env: { ...process.env, SITE_PORT: String(port) }, stdio: ["ignore", "pipe", "pipe"] });
+  const child = spawn(process.execPath, ["site/server.mjs"], { cwd: root, env: { ...process.env, SITE_PORT: String(port), SITE_RELEASE_METADATA_JSON: JSON.stringify(releaseFixture) }, stdio: ["ignore", "pipe", "pipe"] });
   try {
     const base = `http://127.0.0.1:${port}`;
     const health = await (await waitFor(`${base}/health`)).json();
@@ -58,7 +75,7 @@ test("standalone 1helm.com website serves independent product and documentation
     }
     const gettingStarted = await (await fetch(`${base}/manual/getting-started`)).text();
     assert.match(gettingStarted, /On Windows 11 x64, download the Setup executable/i);
-    assert.match(gettingStarted, /v0\.0\.30 is NotSigned<\/code>/i);
+    assert.match(gettingStarted, /v0\.0\.31 is NotSigned<\/code>/i);
     assert.match(gettingStarted, /connect to an existing HTTPS 1Helm host/i);
     assert.doesNotMatch(gettingStarted, /signed Setup executable/i);
     assert.doesNotMatch(gettingStarted, /withheld/i);
@@ -70,6 +87,12 @@ test("standalone 1helm.com website serves independent product and documentation
     assert.equal(benchmarkSchema.$id, "https://1helm.com/schemas/autonomy-benchmark-v1.json");
     assert.deepEqual(benchmarkSchema.required, ["schema", "product", "kind", "started_at", "finished_at", "deterministic", "scope", "summary", "checks"]);
     assert.equal((await fetch(`${base}/install.sh`)).status, 200);
+    const linuxRelease = await (await fetch(`${base}/api/releases/linux/latest`)).json();
+    assert.deepEqual(linuxRelease, {
+      version: "0.0.31",
+      url: "https://github.com/gitcommit90/1Helm/releases/download/v0.0.31/1Helm-0.0.31-linux-node.tgz",
+      sha256: "c".repeat(64),
+    });
     assert.equal((await fetch(`${base}/../../package.json`)).status, 404);
     const sitemap = await (await fetch(`${base}/sitemap.xml`)).text();
     assert.match(sitemap, /https:\/\/1helm\.com\/manual\/connections/);
@@ -157,12 +180,17 @@ test("installer assets are explicit and syntax-valid", () => {
   assert.match(installer, /snapshot_host_contract[\s\S]*rollback_host_contract[\s\S]*TRANSACTION_ACTIVE/, "fresh and repeat installs restore runtime files and unit state after any transactional failure");
   assert.match(installer, /rollback_host_contract[\s\S]*1helm\.service\.active[\s\S]*api\/setup\/status[\s\S]*restored_healthy/, "installer rollback verifies the restored service before claiming recovery");
   assert.match(installer, /NODE_VERSION="22\.23\.1"/);
-  assert.match(installer, /RELEASE_VERSION="0\.0\.28"/, "fresh installs stay on the deliberately published Linux release until the complete private candidate matrix is owner-approved");
+  assert.match(installer, /RELEASE_METADATA_URL="https:\/\/1helm\.com\/api\/releases\/linux\/latest"/, "fresh installs resolve the complete current release from the product site");
+  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, /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");
   assert.match(installer, /need=\([^\n]*flock[^\n]*make[^\n]*c\+\+[^\n]*python3[^\n]*\)/, "the host updater and native dependency toolchain are probed even when download prerequisites already exist");
   assert.match(installer, /import ensurepip[\s\S]*python3-venv/, "the Linux host installs Python's venv support required by durable memory instead of accepting a python3 executable alone");
   assert.doesNotMatch(installer, /npm[^\n]*ci[^\n]*--omit=optional/, "platform-specific optional build packages are retained");
-  assert.match(installer, /EXISTING_SHA="\$\(runuser -u "\$SERVICE_USER" -- git -C "\$RELEASE_ROOT" rev-parse HEAD/, "repeat installs inspect the service-owned release as the service user");
+  assert.match(installer, /EXISTING_VERSION=.*package\.json[\s\S]*EXISTING_VERSION.*VERSION/, "repeat installs verify the retained release version");
   assert.doesNotMatch(installer, /chown -R[^\n]*\$STATE_ROOT/, "repeat installs never recursively rewrite root-owned OCI channel storage");
   assert.match(installer, /chown -R "\$SERVICE_USER:\$SERVICE_USER" "\$RELEASE_ROOT"/, "the extracted application release remains service-owned");
   assert.match(installer, /RELEASES_ROOT=.*releases/);
@@ -208,6 +236,8 @@ test("installer assets are explicit and syntax-valid", () => {
   assert.match(ociRecipe, /docker\.io\/library\/ubuntu:24\.04@sha256:[a-f0-9]{64}/, "the OCI guest base is fully qualified and digest-pinned without mutable short-name state");
   assert.match(ociHelper, /--network-config-dir "\$NETWORKS_ROOT" --tmpdir "\$LIBPOD_TMP"/, "Podman persistent network configuration and libpod scratch stay inside 1Helm-owned roots");
   assert.match(ociHelper, /podman_image\(\)[^\n]*localhost/, "the helper maps 1Helm's portable local image identity to Podman's explicit localhost transport");
+  assert.match(ociHelper, /defaultNetworkBackend[\s\S]*cat "\$STORAGE_ROOT\/defaultNetworkBackend"[\s\S]*printf '%s' netavark/, "Podman's backend selector is repaired to the exact netavark token without a trailing newline");
+  assert.doesNotMatch(ociHelper, /printf 'netavark\\n'/, "fresh Ubuntu Podman must never receive a newline-terminated backend selector");
   assert.doesNotMatch([installer, updater, releaseApply, ociInstaller, ociHelper, ociManifest, ociRecipe].join("\n"), /\blxc\b|per-channel WSL|migration-backups/i, "the clean-slate Linux contract has no legacy runtime bridge");
   assert.match(linuxUnits, /ReadWritePaths=[^\n]*\/usr\/libexec(?:\s|$)[^\n]*\/etc\/default(?:\s|$)[^\n]*\/etc\/systemd\/system(?:\s|$)[^\n]*\/etc\/sudoers\.d(?:\s|$)/, "future updater transactions can atomically replace and roll back only the required host-contract parent trees");
   assert.match(updater, /systemd-run[\s\S]*apply-linux-release\.sh[\s\S]*exit 0/, "all post-verification Linux release mutations run in one transient root transaction outside the updater namespace");