diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d36602..7e9cead 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.12] - 2026-07-26 + +### Fixed + +- Linux host updates now apply the verified runtime files, release symlink, + systemd units, restart, health check, and rollback inside one bounded + transient root transaction. This lets a v0.0.11 host escape its obsolete + `ProtectSystem=strict` mount namespace before atomically replacing host + files, while future updater units grant write access only to the required + parent trees. +- Fresh Linux installs create every LXC state/cache root named by the service + unit before systemd builds its private mount namespace, so the first launch + no longer depends on a channel computer having already been provisioned. + ## [0.0.11] - 2026-07-26 ### Added @@ -334,7 +348,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.11...HEAD +[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.12...HEAD +[0.0.12]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.12 [0.0.11]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.11 [0.0.10]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.10 [0.0.9]: https://github.com/gitcommit90/1Helm/releases/tag/v0.0.9 diff --git a/README.md b/README.md index 7512a4b..d1859e4 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,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 narrow workspace mirrors. | | `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `lxc` on Linux, `wsl` on Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. | -| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.11` | Versioned channel-machine image contract. | +| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.12` | Versioned channel-machine image contract. | ### Agent-first JSON CLI diff --git a/package-lock.json b/package-lock.json index f30380e..cce927d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "1helm", - "version": "0.0.11", + "version": "0.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "1helm", - "version": "0.0.11", + "version": "0.0.12", "license": "AGPL-3.0-only", "hasInstallScript": true, "dependencies": { diff --git a/package.json b/package.json index c8af99b..2b35222 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "1helm", "productName": "1Helm", - "version": "0.0.11", + "version": "0.0.12", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/site/public/apply-linux-release.sh b/site/public/apply-linux-release.sh new file mode 100755 index 0000000..a569efb --- /dev/null +++ b/site/public/apply-linux-release.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Apply one already downloaded, SHA-verified, built, and retained 1Helm Linux +# release as a single root transaction outside an older updater unit's mount +# namespace. No URL, command, or arbitrary destination is accepted here. + +INSTALL_ROOT="/opt/1helm" +RELEASES_ROOT="$INSTALL_ROOT/releases" +APP_ROOT="$INSTALL_ROOT/current" +NODE_LINK="$INSTALL_ROOT/node-current" +STATE_ROOT="/var/lib/1helm" +SERVICE_USER="1helm" +SERVICE_NAME="1helm.service" +PORT="8123" +RELEASE_ROOT="$(readlink -f "${1:-}" 2>/dev/null || true)" +TARGET_VERSION="${2:-}" +STATUS_FILE="$STATE_ROOT/host-update-status.json" +HOST_CONTRACT_PATHS=( + /usr/libexec/1helm-lxc-runtime + /usr/libexec/1helm-lxc-net + /etc/1helm/lxc-unprivileged.conf + /etc/1helm/lxc-idmap + /etc/sudoers.d/1helm-lxc-runtime + /etc/default/lxc-net + /etc/subuid + /etc/subgid + /etc/systemd/system/1helm-lxc-net.service + /etc/systemd/system/1helm.service + /etc/systemd/system/1helm-update.service + /etc/systemd/system/1helm-update.path + /opt/1helm/update-host.sh + /opt/1helm/uninstall-host.sh +) +HOST_UNITS=(1helm-lxc-net.service 1helm.service 1helm-update.path) +TRANSACTION_ACTIVE=0 +ROLLING_BACK=0 +TEMP_ROOT="" +PREVIOUS_RELEASE="" + +[[ "${EUID}" -eq 0 ]] || { echo "The Linux release transaction must run as root." >&2; exit 1; } +[[ "$RELEASE_ROOT" == "$RELEASES_ROOT/"* && -d "$RELEASE_ROOT" ]] \ + || { echo "The Linux release transaction requires a verified retained release." >&2; exit 1; } +[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || { echo "The Linux release transaction requires an exact version." >&2; exit 1; } +PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' "$RELEASE_ROOT/package.json" 2>/dev/null || true)" +[[ "$PACKAGE_VERSION" == "$TARGET_VERSION" ]] \ + || { echo "The retained release does not match the requested version." >&2; exit 1; } +[[ -x "$RELEASE_ROOT/site/public/install-lxc-runtime.sh" \ + && -x "$RELEASE_ROOT/site/public/install-linux-units.sh" \ + && -x "$RELEASE_ROOT/site/public/update-host.sh" \ + && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" \ + && -x "$RELEASE_ROOT/scripts/1helm-lxc-runtime" \ + && -x "$RELEASE_ROOT/scripts/1helm-lxc-net" ]] \ + || { echo "The verified release is missing its Linux host contract." >&2; exit 1; } + +TEMP_ROOT="$(mktemp -d)" +chmod 0700 "$TEMP_ROOT" + +json_string() { + "$NODE_LINK/bin/node" -e 'process.stdout.write(JSON.stringify(process.argv[1] || ""))' "$1" +} + +write_status() { + local state="$1" message="$2" error="${3:-}" candidate="$TEMP_ROOT/status.json" + printf '{"mode":"linux-systemd","status":%s,"version":%s,"checked_at":%s,"message":%s,"error":%s}\n' \ + "$(json_string "$state")" "$(json_string "$TARGET_VERSION")" "$(( $(date +%s) * 1000 ))" \ + "$(json_string "$message")" "$([[ -n "$error" ]] && json_string "$error" || printf null)" >"$candidate" + chown "$SERVICE_USER:$SERVICE_USER" "$candidate" + chmod 0600 "$candidate" + mv -f -- "$candidate" "$STATUS_FILE" +} + +snapshot_host_contract() { + install -d -m 0700 "$TEMP_ROOT/files" "$TEMP_ROOT/units" + local path encoded unit + for path in "${HOST_CONTRACT_PATHS[@]}"; do + if [[ -e "$path" || -L "$path" ]]; then + encoded="${path#/}" + install -d -m 0700 "$TEMP_ROOT/files/$(dirname "$encoded")" + cp -a -- "$path" "$TEMP_ROOT/files/$encoded" + fi + done + for unit in "${HOST_UNITS[@]}"; do + systemctl is-enabled "$unit" >"$TEMP_ROOT/units/$unit.enabled" 2>/dev/null || true + systemctl is-active "$unit" >"$TEMP_ROOT/units/$unit.active" 2>/dev/null || true + done +} + +rollback_host_contract() { + local path encoded unit enabled active restored_healthy=1 + ROLLING_BACK=1 + systemctl disable --now 1helm-update.path 1helm-lxc-net.service >/dev/null 2>&1 || true + systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true + for path in "${HOST_CONTRACT_PATHS[@]}"; do + encoded="${path#/}" + rm -f -- "$path" + if [[ -e "$TEMP_ROOT/files/$encoded" || -L "$TEMP_ROOT/files/$encoded" ]]; then + install -d -m 0755 "$(dirname "$path")" + cp -a -- "$TEMP_ROOT/files/$encoded" "$path" + fi + done + if [[ -n "$PREVIOUS_RELEASE" && -d "$PREVIOUS_RELEASE" ]]; then + ln -s "$PREVIOUS_RELEASE" "$TEMP_ROOT/rollback-current" + mv -Tf "$TEMP_ROOT/rollback-current" "$APP_ROOT" + fi + systemctl daemon-reload + for unit in "${HOST_UNITS[@]}"; do + enabled="$(cat "$TEMP_ROOT/units/$unit.enabled" 2>/dev/null || true)" + active="$(cat "$TEMP_ROOT/units/$unit.active" 2>/dev/null || true)" + [[ "$enabled" == "enabled" ]] && systemctl enable "$unit" >/dev/null 2>&1 || true + [[ "$active" == "active" ]] && systemctl start "$unit" >/dev/null 2>&1 || true + done + if [[ "$(cat "$TEMP_ROOT/units/$SERVICE_NAME.active" 2>/dev/null || true)" == "active" ]]; then + restored_healthy=0 + for _ in {1..300}; do + if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then restored_healthy=1; break; fi + sleep 0.2 + done + fi + TRANSACTION_ACTIVE=0 + ROLLING_BACK=0 + [[ "$restored_healthy" -eq 1 ]] +} + +cleanup_transaction() { + local command_status=$? + trap - EXIT + if [[ "$TRANSACTION_ACTIVE" -eq 1 && "$ROLLING_BACK" -eq 0 ]]; then + if rollback_host_contract; then + write_status "error" "1Helm v$TARGET_VERSION failed its host health transaction; the prior healthy release was restored." "Host update failed and was rolled back." || true + else + write_status "error" "1Helm v$TARGET_VERSION failed and the prior host could not be proven healthy after rollback." "Host update and rollback health check failed." || true + fi + fi + [[ -z "$TEMP_ROOT" ]] || rm -rf -- "$TEMP_ROOT" + exit "$command_status" +} +trap cleanup_transaction EXIT + +PREVIOUS_RELEASE="$(readlink -f "$APP_ROOT" 2>/dev/null || true)" +[[ "$PREVIOUS_RELEASE" == "$RELEASES_ROOT/"* && -d "$PREVIOUS_RELEASE" ]] \ + || { echo "The currently installed 1Helm release is not inside the verified release store." >&2; exit 1; } +snapshot_host_contract +TRANSACTION_ACTIVE=1 +write_status "installing" "The host verified v$TARGET_VERSION and is applying one atomic runtime and application transaction." +HELM_HOST_APPLY_DELEGATED=1 "$RELEASE_ROOT/site/public/install-lxc-runtime.sh" "$RELEASE_ROOT" +ln -s "$RELEASE_ROOT" "$TEMP_ROOT/current" +mv -Tf "$TEMP_ROOT/current" "$APP_ROOT" +HELM_HOST_APPLY_DELEGATED=1 "$RELEASE_ROOT/site/public/install-linux-units.sh" "$RELEASE_ROOT" +write_status "restarting" "The host installed v$TARGET_VERSION and is restarting 1Helm." +systemctl restart "$SERVICE_NAME" +healthy=0 +for _ in {1..300}; do + if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then healthy=1; break; fi + sleep 0.2 +done +[[ "$healthy" -eq 1 ]] || { echo "1Helm v$TARGET_VERSION failed its host health check." >&2; exit 1; } +TRANSACTION_ACTIVE=0 +write_status "current" "This 1Helm host is running v$TARGET_VERSION." diff --git a/site/public/install-linux-units.sh b/site/public/install-linux-units.sh index 4ce45ac..b922cc2 100755 --- a/site/public/install-linux-units.sh +++ b/site/public/install-linux-units.sh @@ -12,9 +12,24 @@ NODE_LINK="$INSTALL_ROOT/node-current" STATE_ROOT="/var/lib/1helm" SERVICE_USER="1helm" +# Bridge upgrades from v0.0.11's too-narrow ProtectSystem=strict namespace. +# The retained release has already been SHA-verified and built by the root +# updater; only its fixed unit installer can be delegated. +if [[ "${HELM_HOST_APPLY_DELEGATED:-}" != "1" ]] \ + && awk -F: '$1 == "0" && $3 ~ /(^|\/)1helm-update\.service(\/|$)/ { found=1 } END { exit found ? 0 : 1 }' /proc/self/cgroup; then + RESOLVED_RELEASE="$(readlink -f "$RELEASE_ROOT" 2>/dev/null || true)" + [[ "$RESOLVED_RELEASE" == /opt/1helm/releases/* && -d "$RESOLVED_RELEASE" ]] \ + || { echo "The updater can delegate only a verified retained 1Helm release." >&2; exit 1; } + DELEGATE_UNIT="1helm-linux-units-apply-${RANDOM}-$$" + exec systemd-run --quiet --collect --wait --pipe --unit="$DELEGATE_UNIT" \ + --property=Type=oneshot --property=NoNewPrivileges=false --property=PrivateTmp=true --property=ProtectHome=true \ + --setenv=HELM_HOST_APPLY_DELEGATED=1 \ + "$RESOLVED_RELEASE/site/public/install-linux-units.sh" "$RESOLVED_RELEASE" +fi + [[ "${EUID}" -eq 0 ]] || { echo "The Linux service installer must run as root." >&2; exit 1; } [[ -n "$RELEASE_ROOT" && -d "$RELEASE_ROOT" ]] || { echo "A verified 1Helm release directory is required." >&2; exit 1; } -[[ -x "$RELEASE_ROOT/site/public/update-host.sh" && -x "$RELEASE_ROOT/site/public/migrate-linux-host-contract.sh" && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" ]] \ +[[ -x "$RELEASE_ROOT/site/public/update-host.sh" && -x "$RELEASE_ROOT/site/public/apply-linux-release.sh" && -x "$RELEASE_ROOT/site/public/migrate-linux-host-contract.sh" && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" ]] \ || { echo "The verified 1Helm release is missing its host lifecycle scripts." >&2; exit 1; } id "$SERVICE_USER" >/dev/null 2>&1 || { echo "The 1Helm service account does not exist." >&2; exit 1; } @@ -67,7 +82,10 @@ NoNewPrivileges=false PrivateTmp=true ProtectHome=true ProtectSystem=strict -ReadWritePaths=$INSTALL_ROOT $STATE_ROOT /var/lib/1helm-lxc /var/cache/1helm-lxc /run/lxc /usr/libexec/1helm-lxc-runtime /usr/libexec/1helm-lxc-net /etc/1helm /etc/default/lxc-net /etc/systemd/system/1helm-lxc-net.service /etc/systemd/system/1helm.service /etc/systemd/system/1helm-update.service /etc/systemd/system/1helm-update.path /etc/sudoers.d/1helm-lxc-runtime /etc/subuid /etc/subgid +# Runtime and unit files are installed by atomic rename and removed during a +# failed-update rollback, so their exact parent directories—not merely the old +# files—must be writable inside this root-owned transaction. +ReadWritePaths=$INSTALL_ROOT $STATE_ROOT /var/lib/1helm-lxc /var/cache/1helm-lxc /run/lxc /usr/libexec /etc/1helm /etc/default /etc/systemd/system /etc/sudoers.d /etc/subuid /etc/subgid EOF install -m 0644 /dev/stdin /etc/systemd/system/1helm-update.path </dev/null || true)" + [[ "$RELEASE_ROOT" == /opt/1helm/releases/* && -d "$RELEASE_ROOT" ]] \ + || { echo "The updater can delegate only a verified retained 1Helm release." >&2; exit 1; } + [[ -x "$RELEASE_ROOT/site/public/apply-linux-release.sh" ]] \ + || { echo "The retained release is missing its atomic Linux transaction." >&2; exit 1; } + TARGET_VERSION="$(/opt/1helm/node-current/bin/node -p 'require(process.argv[1]).version' "$RELEASE_ROOT/package.json" 2>/dev/null || true)" + [[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || { echo "The retained release has no valid package version." >&2; exit 1; } + LEGACY_UPDATER_PID="$(systemctl show --property=MainPID --value 1helm-update.service 2>/dev/null || true)" + legacy_updater_pid_is_exact() { + local pid="${1:-}" main_pid command_line + [[ "$pid" =~ ^[0-9]+$ ]] && ((pid > 1)) && [[ -r "/proc/$pid/cgroup" && -r "/proc/$pid/cmdline" ]] || return 1 + main_pid="$(systemctl show --property=MainPID --value 1helm-update.service 2>/dev/null || true)" + [[ "$main_pid" == "$pid" ]] || return 1 + awk -F: '$1 == "0" && $3 ~ /(^|\/)1helm-update\.service(\/|$)/ { found=1 } END { exit found ? 0 : 1 }' "/proc/$pid/cgroup" || return 1 + command_line="$(tr '\0' '\n' <"/proc/$pid/cmdline")" + grep -Fxq '/opt/1helm/update-host.sh' <<<"$command_line" + } + legacy_updater_pid_is_exact "$LEGACY_UPDATER_PID" \ + || { echo "The legacy updater handoff could not validate its exact systemd main process." >&2; exit 1; } + DELEGATE_UNIT="1helm-release-apply-${TARGET_VERSION//./-}-${RANDOM}-$$" + if systemd-run --quiet --collect --wait --pipe --unit="$DELEGATE_UNIT" \ + --property=Type=oneshot --property=NoNewPrivileges=false --property=PrivateTmp=true --property=ProtectHome=true \ + "$RELEASE_ROOT/site/public/apply-linux-release.sh" "$RELEASE_ROOT" "$TARGET_VERSION"; then + exit 0 + fi + # The delegated transaction has already restored the exact prior release, + # reloaded its units, proved its HTTP health, and written the visible error. + # Stop only the still-identical legacy updater main process with SIGKILL so + # its EXIT trap cannot attempt a second rollback from the obsolete namespace + # or overwrite that stronger evidence. + if legacy_updater_pid_is_exact "$LEGACY_UPDATER_PID"; then + kill -KILL "$LEGACY_UPDATER_PID" + else + echo "The failed release was rolled back, but the legacy updater process identity changed before it could be stopped." >&2 + fi + exit 1 +fi + case "$(uname -m)" in x86_64|amd64) IMAGE_ARCH="amd64" @@ -50,6 +99,12 @@ for command in curl sha256sum lxc-create lxc-attach lxc-info lxc-start lxc-stop done python3 -c 'import ensurepip' >/dev/null 2>&1 || { echo "Python venv support is unavailable after host setup." >&2; exit 1; } +# The service unit names these exact writable roots. They must exist before +# systemd creates the service's mount namespace, even before the first channel +# computer is provisioned. +install -d -o root -g root -m 0711 "$LXC_ROOT" "$LXC_PATH" +install -d -o root -g root -m 0700 "$CACHE_BASE" + TEMP_ROOT="$(mktemp -d)" trap 'rm -rf -- "$TEMP_ROOT"' EXIT ASSET_URL="https://images.linuxcontainers.org/images/ubuntu/noble/$IMAGE_ARCH/default/$IMAGE_BUILD" diff --git a/site/public/update-host.sh b/site/public/update-host.sh index 46c3139..2c41c72 100755 --- a/site/public/update-host.sh +++ b/site/public/update-host.sh @@ -225,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/migrate-linux-host-contract.sh" && -x "$STAGE/site/public/install-lxc-runtime.sh" && -x "$STAGE/site/public/install-linux-units.sh" && -x "$STAGE/site/public/uninstall-host.sh" && -x "$STAGE/scripts/1helm-lxc-runtime" && -x "$STAGE/scripts/1helm-lxc-net" ]] \ +[[ -x "$STAGE/site/public/apply-linux-release.sh" && -x "$STAGE/site/public/migrate-linux-host-contract.sh" && -x "$STAGE/site/public/install-lxc-runtime.sh" && -x "$STAGE/site/public/install-linux-units.sh" && -x "$STAGE/site/public/uninstall-host.sh" && -x "$STAGE/scripts/1helm-lxc-runtime" && -x "$STAGE/scripts/1helm-lxc-net" ]] \ || fail "The verified Linux artifact is missing its isolated LXC runtime contract." chown -R "$SERVICE_USER:$SERVICE_USER" "$STAGE" @@ -247,26 +247,15 @@ else fi chown -R "$SERVICE_USER:$SERVICE_USER" "$RELEASE_ROOT" -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 -"$RELEASE_ROOT/site/public/install-lxc-runtime.sh" "$RELEASE_ROOT" \ - || fail "The verified 1Helm release could not install its isolated LXC runtime." -ln -s "$RELEASE_ROOT" "$TEMP_ROOT/current" -mv -Tf "$TEMP_ROOT/current" "$APP_ROOT" -"$RELEASE_ROOT/site/public/install-linux-units.sh" "$RELEASE_ROOT" \ - || fail "The verified 1Helm release could not migrate its Linux service contract." - -write_status "restarting" "$TARGET_VERSION" "The host installed v$TARGET_VERSION and is restarting 1Helm." -systemctl restart "$SERVICE_NAME" -healthy=0 -for _ in {1..300}; do - if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then healthy=1; break; fi - sleep 0.2 -done -if [[ "$healthy" -ne 1 ]]; then - fail "1Helm v$TARGET_VERSION failed its host health check; the previous release was restored when available." +# One verified transient root transaction owns the runtime files, current +# symlink, unit migration, restart, health check, and rollback together. This +# also escapes v0.0.11's obsolete ProtectSystem=strict mount namespace before +# the first host file is replaced. +APPLY_UNIT="1helm-release-apply-${TARGET_VERSION//./-}-$$" +if ! systemd-run --quiet --collect --wait --pipe --unit="$APPLY_UNIT" \ + --property=Type=oneshot --property=NoNewPrivileges=false --property=PrivateTmp=true --property=ProtectHome=true \ + "$RELEASE_ROOT/site/public/apply-linux-release.sh" "$RELEASE_ROOT" "$TARGET_VERSION"; then + echo "1Helm v$TARGET_VERSION failed its atomic host transaction; the transaction's visible status records whether rollback was proven healthy." >&2 + exit 1 fi -TRANSACTION_ACTIVE=0 -write_status "current" "$TARGET_VERSION" "This 1Helm host is running v$TARGET_VERSION." +exit 0 diff --git a/src/server/channel-computers.ts b/src/server/channel-computers.ts index 924e629..4d9bb40 100644 --- a/src/server/channel-computers.ts +++ b/src/server/channel-computers.ts @@ -67,7 +67,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.11"; +export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.12"; const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[]; const LXC_RUNTIME_VERSION = "1helm-lxc-runtime-v1"; const LXC_HELPER_CANDIDATES = [ diff --git a/src/server/db.ts b/src/server/db.ts index ee3bb3f..c2828e2 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -979,7 +979,7 @@ export function migrate(): void { const platformBackend = process.platform === "darwin" ? "apple" : process.platform === "win32" ? "wsl" : "lxc"; const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend); const backend = ["apple", "lxc", "wsl", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend; - const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.11"); + const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.12"); // Earlier Linux/Windows releases persisted the compatibility `native` // seam into every channel row. A production host update must actually // move those rows onto the platform isolation backend; changing the unit's diff --git a/test/channel-computers.mjs b/test/channel-computers.mjs index b233c19..556d47f 100644 --- a/test/channel-computers.mjs +++ b/test/channel-computers.mjs @@ -168,7 +168,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.11"); + assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.12"); 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 db32c17..e80550d 100644 --- a/test/site.mjs +++ b/test/site.mjs @@ -146,13 +146,14 @@ test("installer assets are explicit and syntax-valid", () => { assert.match(installer, /1helm-update\.path/, "standard Linux installs watch the private host update request file"); const updater = readFileSync(`${root}/site/public/update-host.sh`, "utf8"); const linuxUnits = readFileSync(`${root}/site/public/install-linux-units.sh`, "utf8"); + const releaseApply = readFileSync(`${root}/site/public/apply-linux-release.sh`, "utf8"); 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, /mv -Tf .*current/); - assert.match(updater, /previous release was restored/i); + 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"); - assert.match(updater, /PREVIOUS_RELEASE[\s\S]*== "\$RELEASES_ROOT\/"\*[\s\S]*\{1\.\.300\}/, "updates reject unsafe rollback links and use the same bounded startup allowance"); + assert.match(releaseApply, /PREVIOUS_RELEASE[\s\S]*== "\$RELEASES_ROOT\/"\*[\s\S]*\{1\.\.300\}/, "the atomic update transaction rejects unsafe rollback links and uses the same bounded startup allowance"); assert.match(updater, /\$RELEASE_ROOT\/site\/public\/install-lxc-runtime\.sh/, "the updater installs runtime files from the retained release after staging moves"); assert.match(updater, /install-linux-units\.sh/, "updates migrate the host service contract instead of retaining an obsolete unit"); assert.match(updater, /CURRENT_VERSION[\s\S]*HELM_CHANNEL_COMPUTER_BACKEND=lxc[\s\S]*application is current; the host is migrating its verified runtime contract/i, "an older updater can hand off same-version host-contract migration to the newly installed release"); @@ -170,6 +171,16 @@ test("installer assets are explicit and syntax-valid", () => { assert.match(lxcInstaller, /visudo -cf/, "the minimal helper-only sudo policy is validated before installation"); assert.match(lxcInstaller, /apt-get install[\s\S]*python3-venv[\s\S]*lxc/, "0.0.5 upgrades can install the LXC and durable-memory prerequisites the older host did not have"); assert.match(lxcInstaller, /namespace_covers_range[\s\S]*65536 65535/, "the installer negotiates a full bare-metal or safe nested-host subordinate ID range"); + assert.match(lxcInstaller, /1helm-update\\\.service[\s\S]*systemd-run[\s\S]*apply-linux-release\.sh/, "a v0.0.11 updater hands the complete verified release transaction outside its obsolete read-only mount namespace"); + assert.match(lxcInstaller, /MainPID[\s\S]*\/proc\/\$pid\/cgroup[\s\S]*\/opt\/1helm\/update-host\.sh[\s\S]*kill -KILL/, "a failed handoff stops only the exact legacy updater main process before its obsolete EXIT rollback can run"); + assert.match(lxcInstaller, /install -d[^\n]*"\$LXC_ROOT" "\$LXC_PATH"[\s\S]*install -d[^\n]*"\$CACHE_BASE"/, "the host creates every service ReadWritePaths root before starting 1Helm"); + assert.match(linuxUnits, /1helm-update\\\.service[\s\S]*systemd-run[\s\S]*HELM_HOST_APPLY_DELEGATED/, "a v0.0.11 updater delegates the verified unit migration outside its obsolete mount namespace"); + 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.doesNotMatch(linuxUnits, /ReadWritePaths=[^\n]*\/usr\/libexec\/1helm-lxc-runtime/, "the updater no longer mistakes a writable destination file for atomic parent-directory authority"); + 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"); + assert.match(releaseApply, /RELEASE_ROOT.*RELEASES_ROOT[\s\S]*snapshot_host_contract[\s\S]*install-lxc-runtime\.sh[\s\S]*mv -Tf[\s\S]*install-linux-units\.sh[\s\S]*api\/setup\/status/, "the delegated release transaction owns runtime, source switch, units, and health together"); + assert.match(releaseApply, /rollback_host_contract[\s\S]*rollback-current[\s\S]*SERVICE_NAME\.active[\s\S]*api\/setup\/status/, "a failed delegated release restores the exact prior source and proves its service healthy"); + assert.doesNotMatch(releaseApply, /https?:\/\/(?!127\.0\.0\.1)|\beval\b|curl[^\n]*\|[^\n]*(?:sh|bash)/, "the privileged release transaction never fetches or evaluates remote code"); assert.match(updater, /systemd-run[\s\S]*migrate-linux-host-contract\.sh/, "a verified transient root transaction migrates host files outside the older updater's read-only namespace"); assert.match(hostMigration, /RELEASE_ROOT.*RELEASES_ROOT[\s\S]*snapshot_host_contract[\s\S]*rollback_host_contract[\s\S]*\{1\.\.300\}/, "the fixed host migration accepts only retained releases and rolls back the complete contract after a bounded health failure"); assert.match(hostMigration, /rollback_host_contract[\s\S]*SERVICE_NAME\.active[\s\S]*api\/setup\/status[\s\S]*restored_healthy/, "rollback is not reported as complete until the restored service is HTTP-healthy");