Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.28] - 2026-07-29

### Fixed

- Fresh Linux channel provisioning now waits for the new LXC guest to acquire
its private DHCP address, default route, and working DNS before package
bootstrap. A channel no longer repeatedly creates and cleans up its computer
after starting `apt` during the guest-network race.

- The host installer now prepares the same `/var/lib/1helm-lxc/machines` tree
used by the runtime instead of leaving an obsolete empty `containers` tree.

- Includes the durable installer payload and API-quota corrections from the
superseded Linux-only `0.0.27` prerelease.

## [0.0.27] - 2026-07-29

### Fixed
Expand Down Expand Up @@ -786,7 +801,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.27...HEAD
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.28...HEAD
[0.0.28]: https://github.com/gitcommit90/1Helm/compare/v0.0.27...v0.0.28
[0.0.27]: https://github.com/gitcommit90/1Helm/compare/v0.0.26...v0.0.27
[0.0.26]: https://github.com/gitcommit90/1Helm/compare/v0.0.23...v0.0.26
[0.0.25]: https://github.com/gitcommit90/1Helm/compare/v0.0.23...v0.0.25
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,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.27` | Versioned channel-machine image contract. |
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.28` | Versioned channel-machine image contract. |

### Agent-first JSON CLI

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "1helm",
"productName": "1Helm",
"version": "0.0.27",
"version": "0.0.28",
"private": true,
"type": "module",
"license": "AGPL-3.0-only",
Expand Down
27 changes: 24 additions & 3 deletions scripts/1helm-lxc-runtime
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ start() {
done
die "container did not start"
}
wait_for_guest_network() {
local name="$1" attempt
for attempt in {1..300}; do
if lxc-attach -P "$LXC_PATH" -n "$name" --clear-env -- /bin/sh -c '
ip -4 -o addr show dev eth0 scope global | grep -q " inet 10\.0\.3\." \
&& ip -4 route show default | grep -q " via 10\.0\.3\.1 dev eth0"
' >/dev/null 2>&1; then
lxc-attach -P "$LXC_PATH" -n "$name" --clear-env -- /bin/sh -c '
rm -f /etc/resolv.conf
printf "nameserver 10.0.3.1\n" >/etc/resolv.conf
'
if lxc-attach -P "$LXC_PATH" -n "$name" --clear-env -- /bin/sh -c '
getent ahostsv4 archive.ubuntu.com >/dev/null \
&& getent ahostsv4 security.ubuntu.com >/dev/null
' >/dev/null 2>&1; then
return
fi
fi
sleep 0.2
done
die "container started but its private DHCP route and DNS did not become ready"
}
Comment on lines +79 to +102

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the lxc-attach and DNS probes, not only the loop.

The 300-iteration loop is not a hard timeout: a stuck lxc-attach or getent resolver query can block an iteration, causing installation to exceed the intended ~60-second retry window by potentially many minutes. Add a per-probe timeout and treat timeout failures as retries/final failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/1helm-lxc-runtime` around lines 79 - 100, The wait_for_guest_network
function only bounds retries, not individual lxc-attach or DNS operations. Add a
per-probe timeout to each lxc-attach invocation, including the getent DNS check,
so stalled commands fail and the loop continues toward its existing final die
path without changing the retry behavior.

marker() {
local name="$1" current
if [[ "$(state "$name")" == "RUNNING" ]]; then
Expand Down Expand Up @@ -224,11 +246,10 @@ case "$operation" in
cpu_set="$(select_cpus "$cpus")"
printf '\nlxc.cgroup2.cpuset.cpus = %s\nlxc.cgroup2.memory.max = %s\n' "$cpu_set" "$((memory_mb * 1024 * 1024))" >>"$config"
start "$name"
wait_for_guest_network "$name"
lxc-attach -P "$LXC_PATH" -n "$name" --clear-env -- /bin/sh -eu -c '
export DEBIAN_FRONTEND=noninteractive
rm -f /etc/resolv.conf
printf "nameserver 10.0.3.1\n" >/etc/resolv.conf
apt-get update
apt-get update -o APT::Update::Error-Mode=any
apt-get install -y --no-install-recommends bash build-essential ca-certificates coreutils cron curl dbus file findutils git gzip iproute2 iputils-ping jq less locales man-db nano openssh-client procps python3 python3-pip rsync sudo systemd systemd-sysv tar tzdata unzip vim-tiny wget xz-utils zip
apt-get clean
rm -rf /var/lib/apt/lists/*
Expand Down
4 changes: 2 additions & 2 deletions site/public/install-lxc-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ APP_SOURCE="${1:-}"
INSTALL_ROOT="/opt/1helm"
RUNTIME_ROOT="$INSTALL_ROOT/runtime/lxc"
LXC_ROOT="/var/lib/1helm-lxc"
LXC_PATH="$LXC_ROOT/containers"
LXC_PATH="$LXC_ROOT/machines"
CACHE_BASE="/var/cache/1helm-lxc"
NETWORK_STATE="$LXC_ROOT/network"
HELPER_PATH="/usr/libexec/1helm-lxc-runtime"
Expand All @@ -20,7 +20,7 @@ IDMAP_PATH="/etc/1helm/lxc-idmap"
SUDOERS_PATH="/etc/sudoers.d/1helm-lxc-runtime"
SERVICE_USER="1helm"
IMAGE_BUILD="20260726_07:42"
IMAGE_RELEASE="0.0.27"
IMAGE_RELEASE="0.0.28"

# v0.0.11's updater unit made the exact destination files writable under
# ProtectSystem=strict. Atomic replacement still requires write access to each
Expand Down
2 changes: 1 addition & 1 deletion site/public/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ NODE_LINK="$INSTALL_ROOT/node-current"
STATE_ROOT="/var/lib/1helm"
SERVICE_USER="1helm"
NODE_VERSION="22.23.1"
RELEASE_VERSION="0.0.27"
RELEASE_VERSION="0.0.28"
HOST_CONTRACT_PATHS=(
/usr/libexec/1helm-lxc-runtime
/usr/libexec/1helm-lxc-net
Expand Down
2 changes: 1 addition & 1 deletion src/server/channel-computers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.27";
export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.28";
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 = [
Expand Down
2 changes: 1 addition & 1 deletion src/server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,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.27");
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.28");
// 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
Expand Down
2 changes: 1 addition & 1 deletion test/channel-computers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.27");
assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.28");
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");
Expand Down
8 changes: 7 additions & 1 deletion test/site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ 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\.27"/, "fresh installs use the deliberately published Linux release instead of a rate-limited API lookup");
assert.match(installer, /RELEASE_VERSION="0\.0\.28"/, "fresh installs use the deliberately published Linux release instead of a rate-limited API lookup");
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");
Expand Down Expand Up @@ -172,6 +172,9 @@ test("installer assets are explicit and syntax-valid", () => {
const lxcNetwork = readFileSync(`${root}/scripts/1helm-lxc-net`, "utf8");
const lxcConfig = readFileSync(`${root}/deploy/1helm-lxc-unprivileged.conf`, "utf8");
const uninstaller = readFileSync(`${root}/site/public/uninstall-host.sh`, "utf8");
assert.match(lxcInstaller, /LXC_PATH="\$LXC_ROOT\/machines"/, "installation and the runtime use the same LXC state directory");
assert.match(lxcHelper, /LXC_PATH="\$LXC_ROOT\/machines"/, "the runtime reads the installer-created LXC state directory");
assert.doesNotMatch(lxcInstaller, /LXC_ROOT\/containers/, "the obsolete mismatched LXC state directory is not installed");
Comment on lines +175 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Also reject obsolete paths in the runtime helper.

The test verifies that lxcHelper defines the new LXC_PATH, but only checks for $LXC_ROOT/containers in lxcInstaller. A separate hardcoded obsolete reference in the helper could therefore pass this regression test.

Suggested assertion
  assert.doesNotMatch(lxcInstaller, /LXC_ROOT\/containers/, "the obsolete mismatched LXC state directory is not installed");
+ assert.doesNotMatch(lxcHelper, /LXC_ROOT\/containers/, "the runtime helper does not use the obsolete LXC state directory");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert.match(lxcInstaller, /LXC_PATH="\$LXC_ROOT\/machines"/, "installation and the runtime use the same LXC state directory");
assert.match(lxcHelper, /LXC_PATH="\$LXC_ROOT\/machines"/, "the runtime reads the installer-created LXC state directory");
assert.doesNotMatch(lxcInstaller, /LXC_ROOT\/containers/, "the obsolete mismatched LXC state directory is not installed");
assert.match(lxcInstaller, /LXC_PATH="\$LXC_ROOT\/machines"/, "installation and the runtime use the same LXC state directory");
assert.match(lxcHelper, /LXC_PATH="\$LXC_ROOT\/machines"/, "the runtime reads the installer-created LXC state directory");
assert.doesNotMatch(lxcInstaller, /LXC_ROOT\/containers/, "the obsolete mismatched LXC state directory is not installed");
assert.doesNotMatch(lxcHelper, /LXC_ROOT\/containers/, "the runtime helper does not use the obsolete LXC state directory");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/site.mjs` around lines 175 - 177, Extend the regression assertions
around lxcHelper to also reject the obsolete $LXC_ROOT/containers path,
alongside the existing lxcInstaller check. Keep the current positive LXC_PATH
assertions unchanged and use the same does-not-match behavior and descriptive
message for both runtime and installer content.

assert.match(lxcInstaller, /20260726_07:42/);
assert.match(lxcInstaller, /9c23724d6d22b3a5adf5d0f79d7e3779ded16a6d45f928bce93e14c48113d955/);
assert.match(lxcInstaller, /d5351325dc23e344c4974d7ff546e5e0c91b8e47a9caeb26f39cdc60eaad19e8/);
Expand All @@ -196,6 +199,9 @@ test("installer assets are explicit and syntax-valid", () => {
assert.doesNotMatch(hostMigration, /https?:\/\/(?!127\.0\.0\.1)|\beval\b|curl[^\n]*\|[^\n]*(?:sh|bash)/, "the privileged host migration never fetches or evaluates remote code");
assert.match(lxcHelper, /ownership marker does not match/);
assert.match(lxcHelper, /cleanup_incomplete_create[\s\S]*rm -rf -- "\$LXC_PATH\/\$name"[\s\S]*created=1[\s\S]*lxc-create/, "a failed create removes only its validated partial container directory");
assert.match(lxcHelper, /wait_for_guest_network[\s\S]*10\\\.0\\\.3\\\.[\s\S]*via 10\\\.0\\\.3\\\.1 dev eth0/, "fresh LXC provisioning waits for its DHCP address and default route");
assert.match(lxcHelper, /archive\.ubuntu\.com[\s\S]*security\.ubuntu\.com/, "fresh LXC provisioning proves DNS before package bootstrap");
assert.match(lxcHelper, /start "\$name"[\s\S]*wait_for_guest_network "\$name"[\s\S]*apt-get update -o APT::Update::Error-Mode=any/, "package bootstrap begins only after bounded guest-network readiness");
assert.match(lxcHelper, /cpuset\.cpus\.effective/, "LXC CPU limits are selected from the service's actually delegated host CPUs");
assert.match(lxcHelper, /cpu_count/, "LXC inspection counts noncontiguous delegated CPU lists correctly");
assert.match(lxcNetwork, /1helm-lxc-net-owned/, "the bridge wrapper stops only a bridge it started");
Expand Down
Loading